AVFoundation 媒體創(chuàng)建和編輯

1 媒體的組合和編輯

AVFoundation提供了大量API來創(chuàng)建非線性奠旺、無損的編輯工具和應(yīng)用程序蜘澜。

1.1 組合媒體核心類

組合媒體的核心類時AVComposition。AVAsset和具體的媒體文件是一一對應(yīng)的映射關(guān)系响疚,組合更像是一個說明鄙信,描述多個資源如何正確的呈現(xiàn)和處理。AVComposition沒有遵守NSCoding協(xié)議稽寒,因此不能直接保存數(shù)據(jù)庫扮碧,只能保存必要的屬性,在需要時創(chuàng)建杏糙。

1.2 時間的處理

CMTime
AVFoundation中媒體資源時間的處理為了保存精度慎王,使用CMTime結(jié)構(gòu)體,其中包含value宏侍、timeScale赖淤、flags、epoch四個屬性谅河,value和timeScale分別是64和32位有符號的整形變量咱旱,具體的時間等于value/timeScale。Flags標識數(shù)據(jù)是否有效绷耍、不確定或者是否出現(xiàn)舍入值等吐限。對于視頻資源timeScale通常設(shè)置為常見視頻幀率的公倍數(shù)600,對于音頻資源常設(shè)置為采樣率褂始,如44100(44.1Hz)等诸典。時間的加減法使用CMTimeAdd和CMTimeSubtract,時間的比較使用CMTimeCompareIsInline崎苗。

CMTimeRange
CMTimeRange表示一個時間尺度狐粱,可以使用CMTimeRangeMake函數(shù)初始化,也可以使用CMTimeRangeFromTimeToTime初始化胆数。時間尺度取交集使用GetInterSection肌蜻,取并集使用GetUnion。

1.3 AVURLAsset

播放媒體資源的時候通常直接創(chuàng)建AVAsset必尼,但是在編輯媒體資源時需要創(chuàng)建其子類對象AVURLAsset蒋搜,并在實例方法的options中配置AVURL...Duration...TimeKey為YES,這樣在后續(xù)獲取asset時間相關(guān)屬性時更精確,盡管這需要增加開銷豆挽。

1.4 組合媒體


首先需要定義組合媒體中需要用到的類酸休,CompositionBuilderFactory負責管理CompositionBuilder,CompositionBuilder負責具體的創(chuàng)建組合對象并將創(chuàng)建好的AVComposition封裝到THBaseComposition中祷杈,THBaseComposition負責將返回可播放的AVPlayerItem對象或者導出的預(yù)設(shè)值斑司。THCompositionExporter包含一個THBaseComposition對象,負責將組合導出成mov文件但汞。

THTimeLine對象表示一個時間軸對象宿刮,由多個THTimelineItem組成黄琼,每個THTimelineItem對象表示時間軸上的一個資源貌夕。其中鍵值著資源在時間軸上的位置。為了更好的載入媒體資源食绿,創(chuàng)建子類THMediaItem踩叭,它封裝了AVAsset對象磕潮,可以載入track等資源,為視頻編輯做好準備容贝。為了區(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時候斤富,AVFoundation會自動管理軌道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ù)叠纷,因此其屬性有具體的時間
            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 導出媒體

**初始化**
@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 混合音頻

當有多個音頻軌道時,如音樂軌道和配音軌道潦嘶,通常希望在有配音時涩嚣,背景音樂音量降低。在AVFoundation中AVAudioMix及其相關(guān)類負責音頻軌道的音量處理衬以。在AVAudioMix中軌道的音量大小在0~1之間缓艳,默認的行為是每個軌道都以最大音量1播放校摩。AVMutable...Parameters提供了兩個方法用于立即設(shè)置音量到某個值看峻,和在某個范圍內(nèi)將值由一個值設(shè)置到另外一個值。對于組合衙吩,對于本地媒體資源互妓,對于媒體輸出都可以設(shè)置AVAudioMix來控制音頻播放和輸出的行為。


1- 初始化THAudioMixComposition負責提供可播放對象和導出預(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負責編輯媒體

@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)建視頻過度效果及組合視頻軌道

3.1 核心類


AVFoundation中AVVideoComposition是描述視頻組合各個視頻軌道應(yīng)該具體如何呈現(xiàn)的核心類。它可以被設(shè)置到AVPlayerItem冯勉、AVAssetExportSession等中澈蚌,以用于播放和導出特定軌道組合效果的視頻對象。AVVideoComposition由多個Instruction構(gòu)成灼狰,每個Instruction描述了一個時間段timeRange宛瞄,其屬性layerInstructions中每一個對象都描述了對應(yīng)軌道視頻資源呈現(xiàn)方式。

在實際操作時交胚,通常先建立AVComposition對象份汗,并在軌道上添加必要的媒體信息,對于多個視頻軌道蝴簇,在導出或者播放AVComposition時杯活,都會在軌道上沒有媒體數(shù)據(jù)地方插入一個空片段。并且導出后視頻僅含有一個視頻軌道熬词。Apple Developer Center中含有一個AVCompositionDebugView的APP示例旁钧,可以顯示軌道信息。幫助調(diào)試程序互拾。

在一個AVComposition中使用多個視頻軌道并且沒有配置AVVideoComposition屬性時歪今,在播放時只有索引為1的軌道會被渲染,導出同樣颜矿。創(chuàng)建AVVideoComposition的方式有兩種彤委。

1)手動創(chuàng)建:直接通過AVVideoComposition的init方法創(chuàng)建,再為其添加Instruction數(shù)組或衡,Instruction包含時間信息焦影,再為Instruction添加layerInstructions屬性,設(shè)置每個軌道的層展示方式封断。設(shè)置AVVideoComposition的rendersize斯辰、frameduration和renderScale,renderScale通常使用默認縮放比1.0坡疼,frameduration通常不設(shè)置彬呻,使用媒體默認幀率。

2)快捷創(chuàng)建:通過AVVideoComposition的videoComposition...OfAsset:類方法創(chuàng)建柄瑰,這種方式穿件的videoComposition會包含闸氮。

  • Instructions:包含完整的基于組合視頻軌道及其中包含片段空間布局的組合和層指令。通常其中默認的層布局指令layerInstruction都是全屏展示教沾,需要對過度的Instruction和layerInstruction重新設(shè)置展示方式蒲跨。
  • redersize:設(shè)置為AVComposition的naturalSize,若其為空授翻,則設(shè)置為最大視頻維度的尺寸值或悲。
  • frameDuration:設(shè)置為組合中最大軌道的nominalFrameRate孙咪,如果所有軌道改值都為0,則設(shè)置為1/30.(30FPS)巡语。
  • renderScale:始終為1翎蹈。

3.1 邏輯實現(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;
        
        // 動畫處理
        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)場指令

- (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)場效果實現(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 動畫圖層內(nèi)容

在視頻處理時,有時候常常希望添加上一些疊加效果男公,如水印荤堪、標題、下沿字母等枢赔。此時需要結(jié)合AVFoundation框架好Core Animation兩個框架來實現(xiàn)這個功能逞力。

4.1 Core Animation簡介

Core Animation是基于GPU提供硬件加速的圖像渲染框架。其主要分為Layers和Animations兩部分糠爬。Core Animation將另起文章研究寇荧。另Lockwood的iOS Core Animation。

  • Layers:基本類為CALayer执隧,用于管理屏幕中可視內(nèi)容元素揩抡。一般指圖片或者Bezier線條。Layer自身也可以設(shè)置可視化屬性镀琉,如背景色等峦嗤。CATextLayer和CAShapeLayer分別用于文字和Bezier曲線的渲染。
  • Animations:其基本類是一個抽象類CAAnimation屋摔,根據(jù)不同的需要定義了CABasicAnimation烁设、CAKeyFrameAnimation等。這里需要注意CAAnimationGroup最好不要結(jié)合AVFoundation使用钓试。

4.2 使用Core Animation

4.2.1 非AVFoundation環(huán)境

普通環(huán)境下装黑,Core Animation渲染圖像的時間是基于主機時間渲染。主機時間是從系統(tǒng)啟動時開始計算并單向向前推進弓熏。

4.2.2 AVFoundation環(huán)境

AVFoundation中時時播放時恋谭,我們需要在預(yù)覽視圖上添加一個AVSynchronizedLayer,并將其關(guān)聯(lián)對應(yīng)的AVPlayerItem挽鞠,這樣在AVSynchronizedLayer上所有的子視圖上的動畫都將依據(jù)對應(yīng)的AVPlayerItem的時間執(zhí)行疚颊,當AVPlayerItem暫停、倒退時動畫都將做出相應(yīng)反饋信认。

AVFoundation中導出CoreAnimation時材义,AVFoundation提供了一個可以整合視頻圖層和動畫圖層的AVVideoCompositionCoreAnimationTool工具類。

AVVideoCompositionCoreAnimationTool可以將組合視頻幀整合于視頻圖層中嫁赏,并在其中渲染動畫效果其掂,但是使用時應(yīng)注意以下兩點。

  • 不能移除動畫:Core Animation的默認行為是執(zhí)行動畫橄教,然后移除動畫清寇,但是在AVFoundation中動畫會被反復執(zhí)行,因此其removedOnCompetition必須設(shè)置為NO护蝶。
  • 開始時間不能設(shè)置為0.0:動畫的開始時間設(shè)置為0.0時將會轉(zhuǎn)換為當前主機的時間CACurrentMeidaTIme()华烟。這樣的時間不會和AVPlayerItem關(guān)聯(lián),動畫將永遠不會執(zhí)行持灰,需要設(shè)置為AVCoreAnimationBeginTimeAtZero常量盔夜。

4.3 在視頻組合中創(chuàng)建動畫

為了整合前幾章內(nèi)容,設(shè)計THTimeLineItem子類THTitleItem來負責創(chuà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 添加動畫效果
- (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 準備視頻組合負責生產(chǎn)可導出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喂链,否則圖片文字會顛倒
        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 準備視頻組合Builder負責生成組合對象

此處大部分邏輯實現(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;
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末椭微,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子盲链,更是在濱河造成了極大的恐慌蝇率,老刑警劉巖,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件刽沾,死亡現(xiàn)場離奇詭異本慕,居然都是意外死亡,警方通過查閱死者的電腦和手機侧漓,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進店門布蔗,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了悍抑?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵记靡,是天一觀的道長嚎花。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任令境,我火速辦了婚禮陈醒,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘朦促。我一直安慰自己洒疚,他們只是感情好,可當我...
    茶點故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布矢棚。 她就那樣靜靜地躺著兜粘,像睡著了一般路鹰。 火紅的嫁衣襯著肌膚如雪趣斤。 梳的紋絲不亂的頭發(fā)上黎休,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天浓领,我揣著相機與錄音玉凯,去河邊找鬼。 笑死联贩,一個胖子當著我的面吹牛漫仆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播泪幌,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼盲厌,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了祸泪?” 一聲冷哼從身側(cè)響起吗浩,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎没隘,沒想到半個月后懂扼,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡右蒲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年阀湿,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片瑰妄。...
    茶點故事閱讀 38,161評論 1 334
  • 序言:一個原本活蹦亂跳的男人離奇死亡陷嘴,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出间坐,到底是詐尸還是另有隱情灾挨,我是刑警寧澤,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布眶诈,位于F島的核電站涨醋,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏逝撬。R本人自食惡果不足惜浴骂,卻給世界環(huán)境...
    茶點故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宪潮。 院中可真熱鬧溯警,春花似錦、人聲如沸狡相。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽尽棕。三九已至喳挑,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背伊诵。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工单绑, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人曹宴。 一個月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓搂橙,卻偏偏與公主長得像,于是被迫代替她去往敵國和親笛坦。 傳聞我的和親對象是個殘疾皇子区转,可洞房花燭夜當晚...
    茶點故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容