iOS 音頻視頻圖像合成那點事

人而無信不知其可

前言

很久很久沒有寫點什么了,只因為最近事情太多了,這幾天終于閑下來了,趁此機會,記錄下幾個月前寫的一個關(guān)于視頻音頻圖片合成方面的一個小例子

入場

先來看看實現(xiàn)的大概功能吧~

功能介紹.png

由于其它功能不好制作gif,這里就先展示一個簡單的水印圖片


水印.gif

下面就讓我們一點點來分析分析

需要了解什么

先來看一個關(guān)系圖,字寫的丑,將就著看吧....


IMG_8292.JPG

看著上面的圖,是有點凌亂的感覺,下面我們就一點點來剝開。

代碼實現(xiàn)

根據(jù)需要實現(xiàn)的功能,我這里建了一個類,來分別實現(xiàn)不同的功能

@interface VideoAudioComposition : NSObject


/**
 合成后的名字
 */
@property (nonatomic,copy) NSString *compositionName;

/**
 合成類型
 */
@property (nonatomic,assign) CompositionType compositionType;


/**
 轉(zhuǎn)換后的格式
 */
@property (nonatomic, copy) AVFileType outputFileType;


/**
 進度block
 */
@property (nonatomic,copy)CompositionProgress progressBlock;

/**
 視頻音頻合成

 @param videoUrl 視頻地址
 @param videoTimeRange 截取時間
 @param audioUrl 音頻地址
 @param audioTimeRange 截取時間
 @param successBlcok 成功回調(diào)
 */
- (void)compositionVideoUrl:(NSURL *)videoUrl videoTimeRange:(CMTimeRange)videoTimeRange audioUrl:(NSURL *)audioUrl audioTimeRange:(CMTimeRange)audioTimeRange success:(SuccessBlcok)successBlcok;


/**
 視頻和視頻合成

 @param videoUrl 視頻地址
 @param videoTimeRange 截取時間
 @param mergeVideoUrl 視頻地址
 @param mergeVideoTimeRange 截取時間
 @param successBlcok 成功回調(diào)
 */
- (void)compositionVideoUrl:(NSURL *)videoUrl videoTimeRange:(CMTimeRange)videoTimeRange mergeVideoUrl:(NSURL *)mergeVideoUrl mergeVideoTimeRange:(CMTimeRange)mergeVideoTimeRange success:(SuccessBlcok)successBlcok;


/**
 多個音頻合成

 @param audios 音頻地址
 @param timeRanges 截取時間(數(shù)組可為空:默認視為音頻的起止時間,若不為空,則必須傳入與audios數(shù)量相等的time)CMTimeRangeMake(kCMTimeZero, kCMTimeZero) 默認為起止時間
 @param successBlcok 成功回調(diào)
 */
- (void)compositionAudios:(NSArray <NSURL*>*)audios timeRanges:(NSArray<NSValue *> *)timeRanges success:(SuccessBlcok)successBlcok;


/**
 多個視頻合成

 @param videos 視頻地址
 @param timeRanges 截取時間(數(shù)組可為空:默認視為視頻的起止時間,若不為空,則必須傳入與audios數(shù)量相等的time)CMTimeRangeMake(kCMTimeZero, kCMTimeZero) 默認為起止時間
 @param successBlcok 成功回調(diào)
 */
- (void)compositionVideos:(NSArray <NSURL*>*)videos timeRanges:(NSArray<NSValue *> *)timeRanges success:(SuccessBlcok)successBlcok;
@end

實現(xiàn)代碼

- (NSString *)compositionPath
{
    return [GLFolderManager createCacheFilePath:kCompositionPath];
}

- (void)compositionVideoUrl:(NSURL *)videoUrl videoTimeRange:(CMTimeRange)videoTimeRange audioUrl:(NSURL *)audioUrl audioTimeRange:(CMTimeRange)audioTimeRange success:(SuccessBlcok)successBlcok
{
    NSCAssert(_compositionName.length > 0, @"請輸入轉(zhuǎn)換后的名字");
    NSString *outPutFilePath = [[self compositionPath] stringByAppendingPathComponent:_compositionName];
    
    //存在該文件
    if ([GLFolderManager fileExistsAtPath:outPutFilePath]) {
        [GLFolderManager clearCachesWithFilePath:outPutFilePath];
    }
    
    // 創(chuàng)建可變的音視頻組合
    AVMutableComposition *composition = [AVMutableComposition composition];
    // 音頻通道
    AVMutableCompositionTrack *audioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
    // 視頻通道 枚舉 kCMPersistentTrackID_Invalid = 0
    AVMutableCompositionTrack *videoTrack = nil;
    
    
    // 視頻采集
    AVURLAsset *videoAsset = [[AVURLAsset alloc] initWithURL:videoUrl options:nil];
    
    videoTimeRange = [self fitTimeRange:videoTimeRange avUrlAsset:videoAsset];
    
    // 音頻采集
    AVURLAsset *audioAsset = [[AVURLAsset alloc] initWithURL:audioUrl options:nil];
    
    audioTimeRange = [self fitTimeRange:audioTimeRange avUrlAsset:audioAsset];

    
    if (_compositionType == VideoAudioToVideo) {
        //以視頻時間為標準 若視頻時間小于音頻時間 則讓音頻時間和視頻時間保持一致
        if (CMTimeCompare(videoTimeRange.duration,audioTimeRange.duration))
        {
            audioTimeRange.duration = videoTimeRange.duration;
        }
        //在測試中發(fā)現(xiàn) VideoAudioToAudio如果不用 視頻通道  就不要去創(chuàng)建 否則會失敗
        videoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
    }

    

    // 音頻采集通道
    AVAssetTrack *audioAssetTrack = [[audioAsset tracksWithMediaType:AVMediaTypeAudio] firstObject];
    // 加入合成軌道之中
    [audioTrack insertTimeRange:audioTimeRange ofTrack:audioAssetTrack atTime:kCMTimeZero error:nil];

    
    switch (_compositionType) {
        case VideoAudioToAudio:
        {
            //  音頻采集通道
            AVAssetTrack *videoAssetTrack = [[videoAsset tracksWithMediaType:AVMediaTypeAudio] firstObject];
            //  把采集軌道數(shù)據(jù)加入到可變軌道之中
            [audioTrack insertTimeRange:videoTimeRange ofTrack:videoAssetTrack atTime:audioTimeRange.duration error:nil];
        }
            break;
        case VideoAudioToVideo:{

            //  視頻采集通道
            AVAssetTrack *videoAssetTrack = [[videoAsset tracksWithMediaType:AVMediaTypeVideo] firstObject];
            //  把采集軌道數(shù)據(jù)加入到可變軌道之中
            [videoTrack insertTimeRange:videoTimeRange ofTrack:videoAssetTrack atTime:kCMTimeZero error:nil];
        }
            break;
        default:
            break;
    }

    [self composition:composition storePath:outPutFilePath success:successBlcok];
}

- (void)compositionVideoUrl:(NSURL *)videoUrl videoTimeRange:(CMTimeRange)videoTimeRange mergeVideoUrl:(NSURL *)mergeVideoUrl mergeVideoTimeRange:(CMTimeRange)mergeVideoTimeRange success:(SuccessBlcok)successBlcok
{
    switch (_compositionType) {
        case VideoToVideo:
        {
            NSArray *timeRanges = [NSArray arrayWithObjects:[NSValue valueWithCMTimeRange:videoTimeRange],[NSValue valueWithCMTimeRange:mergeVideoTimeRange] ,nil];
            [self compositionVideos:@[videoUrl,mergeVideoUrl] timeRanges:timeRanges success:successBlcok];
        }
            break;
        case VideoToAudio:{
            NSArray *timeRanges = [NSArray arrayWithObjects:[NSValue valueWithCMTimeRange:videoTimeRange],[NSValue valueWithCMTimeRange:mergeVideoTimeRange] ,nil];
            [self compositionAudios:@[videoUrl,mergeVideoUrl] timeRanges:timeRanges success:successBlcok];
        }
            break;
        default:
            break;
    }
}

- (void)compositionVideos:(NSArray<NSURL *> *)videos timeRanges:(NSArray<NSValue *> *)timeRanges success:(SuccessBlcok)successBlcok
{
    [self compositionMedia:videos timeRanges:timeRanges type:0 success:successBlcok];
}

- (void)compositionAudios:(NSArray<NSURL *> *)audios timeRanges:(NSArray<NSValue *> *)timeRanges success:(SuccessBlcok)successBlcok
{
    [self compositionMedia:audios timeRanges:timeRanges type:1 success:successBlcok];
}


#pragma mark == private method
- (void)compositionMedia:(NSArray<NSURL *> *)media timeRanges:(NSArray<NSValue *> *)timeRanges type:(NSInteger)type success:(SuccessBlcok)successBlcok
{
    NSCAssert(_compositionName.length > 0, @"請輸入轉(zhuǎn)換后的名字");
    NSCAssert((timeRanges.count == 0 || timeRanges.count == media.count), @"請輸入正確的timeRange");
    NSString *outPutFilePath = [[self compositionPath] stringByAppendingPathComponent:_compositionName];
   
    //存在該文件
    if ([GLFolderManager fileExistsAtPath:outPutFilePath]) {
        [GLFolderManager clearCachesWithFilePath:outPutFilePath];
    }

    // 創(chuàng)建可變的音視頻組合
    AVMutableComposition *composition = [AVMutableComposition composition];
    if (type == 0) {
        // 視頻通道
        AVMutableCompositionTrack *videoTrack = [composition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];
        // 音頻通道
        AVMutableCompositionTrack *audioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
        
        CMTime atTime = kCMTimeZero;
        
        for (int i = 0;i < media.count;i ++) {
            NSURL *url = media[i];
            CMTimeRange timeRange = CMTimeRangeMake(kCMTimeZero, kCMTimeZero);
            if (timeRanges.count > 0) {
                timeRange = [timeRanges[i] CMTimeRangeValue];
            }
            
            // 視頻采集
            AVURLAsset *videoAsset = [[AVURLAsset alloc] initWithURL:url options:nil];
            timeRange = [self fitTimeRange:timeRange avUrlAsset:videoAsset];
            
            // 視頻采集通道
            AVAssetTrack *videoAssetTrack = [[videoAsset tracksWithMediaType:AVMediaTypeVideo] firstObject];
            // 把采集軌道數(shù)據(jù)加入到可變軌道之中
            [videoTrack insertTimeRange:timeRange ofTrack:videoAssetTrack atTime:atTime error:nil];
            
            // 音頻采集通道
            AVAssetTrack *audioAssetTrack = [[videoAsset tracksWithMediaType:AVMediaTypeAudio] firstObject];
            // 加入合成軌道之中
            [audioTrack insertTimeRange:timeRange ofTrack:audioAssetTrack atTime:atTime error:nil];
            
            atTime = CMTimeAdd(atTime, timeRange.duration);
        }
    }else{
        // 音頻通道
        AVMutableCompositionTrack *audioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];
        
        CMTime atTime = kCMTimeZero;
        
        for (int i = 0;i < media.count;i ++) {
            NSURL *url = media[i];
            CMTimeRange timeRange = CMTimeRangeMake(kCMTimeZero, kCMTimeZero);
            if (timeRanges.count > 0) {
                timeRange = [timeRanges[i] CMTimeRangeValue];
            }
            
            // 音頻采集
            AVURLAsset *audioAsset = [[AVURLAsset alloc] initWithURL:url options:nil];
            timeRange = [self fitTimeRange:timeRange avUrlAsset:audioAsset];
            
            // 音頻采集通道
            AVAssetTrack *audioAssetTrack = [[audioAsset tracksWithMediaType:AVMediaTypeAudio] firstObject];
            // 加入合成軌道之中
            [audioTrack insertTimeRange:timeRange ofTrack:audioAssetTrack atTime:atTime error:nil];
            
            atTime = CMTimeAdd(atTime, timeRange.duration);
        }
    }
    [self composition:composition storePath:outPutFilePath success:successBlcok];
}


//得到合適的時間
- (CMTimeRange)fitTimeRange:(CMTimeRange)timeRange avUrlAsset:(AVURLAsset *)avUrlAsset
{
    CMTimeRange fitTimeRange = timeRange;
    
    if (CMTimeCompare(avUrlAsset.duration,timeRange.duration))
    {
        fitTimeRange.duration = avUrlAsset.duration;
    }
    if (CMTimeCompare(timeRange.duration,kCMTimeZero))
    {
        fitTimeRange.duration = avUrlAsset.duration;
    }
    return fitTimeRange;
}

//輸出
- (void)composition:(AVMutableComposition *)avComposition
          storePath:(NSString *)storePath
            success:(SuccessBlcok)successBlcok
{
    // 創(chuàng)建一個輸出
    AVAssetExportSession *assetExport = [[AVAssetExportSession alloc] initWithAsset:avComposition presetName:AVAssetExportPresetHighestQuality];
    assetExport.outputFileType = AVFileTypeQuickTimeMovie;
    // 輸出地址
    assetExport.outputURL = [NSURL fileURLWithPath:storePath];
    // 優(yōu)化
    assetExport.shouldOptimizeForNetworkUse = YES;
    
    __block NSTimer *timer = nil;

    timer = [NSTimer scheduledTimerWithTimeInterval:0.05 repeats:YES block:^(NSTimer * _Nonnull timer) {
        NSLog(@" 打印信息:%f",assetExport.progress);
        if (self.progressBlock) {
            self.progressBlock(assetExport.progress);
        }
    }];


    // 合成完畢
    [assetExport exportAsynchronouslyWithCompletionHandler:^{
        if (timer) {
            [timer invalidate];
            timer = nil;
        }
        // 回到主線程
        switch (assetExport.status) {
            case AVAssetExportSessionStatusUnknown:
                NSLog(@"exporter Unknow");
                break;
            case AVAssetExportSessionStatusCancelled:
                NSLog(@"exporter Canceled");
                break;
            case AVAssetExportSessionStatusFailed:
                NSLog(@"%@", [NSString stringWithFormat:@"exporter Failed%@",assetExport.error.description]);
                break;
            case AVAssetExportSessionStatusWaiting:
                NSLog(@"exporter Waiting");
                break;
            case AVAssetExportSessionStatusExporting:
                NSLog(@"exporter Exporting");
                break;
            case AVAssetExportSessionStatusCompleted:
                NSLog(@"exporter Completed");
                dispatch_async(dispatch_get_main_queue(), ^{
                    // 調(diào)用播放方法
                    successBlcok([NSURL fileURLWithPath:storePath]);
                });
                break;
        }
    }];
}

在該類中涉及的都是一些簡單的音頻與視頻的合成,代碼其實并不負復(fù)雜,難點就在于對涉及的類的理解與運用上面。針對上面的代碼,就簡單分析下。

AVAsset:應(yīng)該指對應(yīng)資源的資源信息
AVURLAsset:繼承自AVAsset,獲取對應(yīng)資源的資源信息,如獲取視頻或者音頻的信息
AVMutableComposition :繼承自AVComposition,而AVComposition繼承自AVAsset,應(yīng)該是專門用來合成音視頻的合成器
AVMutableCompositionTrack :繼承自AVCompositionTrack,而AVCompositionTrack繼承自AVAssetTrack,表示資源文件中的軌道,有音頻軌、視頻軌等,里面可以插入各種對應(yīng)的素材;
AVAssetTrack:素材資源的相關(guān)軌道
AVAssetExportSession:配置相應(yīng)的渲染并進行輸出
1、我們先從輸出端一步一步看:

- (nullable instancetype)initWithAsset:(AVAsset *)asset presetName:(NSString *)presetName

上面是AVAssetExportSession的初始化函數(shù),走初始化函數(shù)中,我們可以看到必須要有一個AVAsset對象,presetName為輸出的質(zhì)量,如下這些

AVF_EXPORT NSString *const AVAssetExportPresetLowQuality         NS_AVAILABLE(10_11, 4_0);
AVF_EXPORT NSString *const AVAssetExportPresetMediumQuality      NS_AVAILABLE(10_11, 4_0);
AVF_EXPORT NSString *const AVAssetExportPresetHighestQuality     NS_AVAILABLE(10_11, 4_0);

AVMutableComposition則繼承自AVAsset,并且是用來合成視頻音頻的,所以我們完全可以通過輸入該對象來實現(xiàn)音視頻的合成。

2、在合成器AVMutableComposition有了之后,我們就需要向其中添加我們需要合成的音頻或者視頻通道了。

    AVMutableCompositionTrack *audioTrack = [composition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];

其中AVMediaTypeAudio代表音頻通道,AVMediaTypeVideo為視頻通道。
3、在音視頻通道有了之后,我們需要獲取我們需要插入的音視頻資源

    // 音頻采集
    AVURLAsset *audioAsset = [[AVURLAsset alloc] initWithURL:audioUrl options:nil];

    // 音頻采集通道
    AVAssetTrack *audioAssetTrack = [[audioAsset tracksWithMediaType:AVMediaTypeAudio] firstObject];

4、在資源采集后,我們需要將其插入我們的音頻或者視頻通道中

            //  把采集軌道數(shù)據(jù)加入到可變軌道之中
            [audioTrack insertTimeRange:videoTimeRange ofTrack:videoAssetTrack atTime:audioTimeRange.duration error:nil];

5、輸出

    // 合成完畢
    [assetExport exportAsynchronouslyWithCompletionHandler:^{
        if (timer) {
            [timer invalidate];
            timer = nil;
        }
        // 回到主線程
        switch (assetExport.status) {
            case AVAssetExportSessionStatusUnknown:
                NSLog(@"exporter Unknow");
                break;
            case AVAssetExportSessionStatusCancelled:
                NSLog(@"exporter Canceled");
                break;
            case AVAssetExportSessionStatusFailed:
                NSLog(@"%@", [NSString stringWithFormat:@"exporter Failed%@",assetExport.error.description]);
                break;
            case AVAssetExportSessionStatusWaiting:
                NSLog(@"exporter Waiting");
                break;
            case AVAssetExportSessionStatusExporting:
                NSLog(@"exporter Exporting");
                break;
            case AVAssetExportSessionStatusCompleted:
                NSLog(@"exporter Completed");
                dispatch_async(dispatch_get_main_queue(), ^{
                    // 調(diào)用播放方法
                    successBlcok([NSURL fileURLWithPath:storePath]);
                });
                break;
        }
    }];

注:在合成中還涉及的有time,這里就不細講了,為了規(guī)避我們自己設(shè)置的時間超出了音視頻本身的時間長度,這里我稍微做了下處理,過濾了下時間

//得到合適的時間
- (CMTimeRange)fitTimeRange:(CMTimeRange)timeRange avUrlAsset:(AVURLAsset *)avUrlAsset
{
    CMTimeRange fitTimeRange = timeRange;
    
    if (CMTimeCompare(avUrlAsset.duration,timeRange.duration))
    {
        fitTimeRange.duration = avUrlAsset.duration;
    }
    if (CMTimeCompare(timeRange.duration,kCMTimeZero))
    {
        fitTimeRange.duration = avUrlAsset.duration;
    }
    return fitTimeRange;
}
分割線

上面是簡單的音視頻的合成,有走視頻中提取音頻然后和其它音頻進行合成,也有音頻與音頻的合成,視頻與視頻的合成....大概如下

typedef NS_ENUM(NSInteger,CompositionType) {
    VideoToVideo = 0,//視頻加視頻頻-視頻(可細分)
    VideoToAudio,//視頻加視頻-音頻
    VideoAudioToVideo,//視頻加音頻-視頻
    VideoAudioToAudio,//視頻加音頻-音頻
    AudioToAudio,//音頻加音頻-音頻
};

由于思路都差不多,只是需要進行提取對應(yīng)的音頻通道或者視頻通道,然后進行時間的設(shè)置和進行對應(yīng)的合并,所以這里就不在多講,下面讓我們繼續(xù)往下看,看看視頻水印圖片合成視頻、視頻截圖等稍微復(fù)雜點的功能。

來,繼續(xù)看

針對這些功能,我又建了一個類,來單獨處理

@interface VideoAudioEdit : NSObject

/**
 進度block
 */
@property (nonatomic,copy)CompositionProgress progressBlock;

/**
 截取視頻某時刻的畫面

 @param videoUrl 視頻地址
 @param requestedTimes cmtime 數(shù)組
 */
- (void)getThumbImageOfVideo:(NSURL *_Nonnull)videoUrl forTimes:(NSArray<NSValue *> *_Nonnull)requestedTimes complete:(CompleteBlock _Nullable )complete;


/**
 將圖片合成為視頻

 @param images 圖片數(shù)組
 @param videoName 視頻名字
 @param successBlcok 視頻地址
 */
- (void)compositionVideoWithImage:(NSArray <UIImage *>*_Nonnull)images videoName:(NSString *_Nonnull)videoName success:(SuccessBlcok _Nullable )successBlcok;



/**
 將圖片合成為視頻 并加上音樂

 @param images 圖片數(shù)組
 @param videoName 視頻名字
 @param audioUrl 音頻地磚
 @param successBlcok 返回視頻地址
 */
- (void)compositionVideoWithImage:(NSArray <UIImage *>*_Nonnull)images videoName:(NSString *_Nonnull)videoName audio:(NSURL*_Nullable)audioUrl success:(SuccessBlcok _Nullable )successBlcok;


/**
 視頻水印

 @param videoUrl 視頻地址
 @param videoName 視頻名字
 @param successBlcok 返回
 */
- (void)watermarkForVideo:(NSURL *_Nonnull)videoUrl videoName:(NSString *_Nonnull)videoName success:(SuccessBlcok _Nullable )successBlcok;

@end
截取視頻某時刻的畫面

這個應(yīng)該是最簡單的,先直接上源碼

- (void)getThumbImageOfVideo:(NSURL *)videoUrl forTimes:(NSArray<NSValue *> *)requestedTimes complete:(CompleteBlock)complete
{
    AVURLAsset *asset = [[AVURLAsset alloc] initWithURL:videoUrl options:nil];
    AVAssetImageGenerator *assetImage = [[AVAssetImageGenerator alloc] initWithAsset:asset];
    //精確截取時間
    assetImage.requestedTimeToleranceAfter = kCMTimeZero;
    assetImage.requestedTimeToleranceBefore = kCMTimeZero;
    assetImage.apertureMode = AVAssetImageGeneratorApertureModeEncodedPixels;
    //設(shè)置最大尺寸
    assetImage.maximumSize = CGSizeMake(640, 400);

    //requestedTime 請求時間 actualTime實際截取畫面的時間
    [assetImage generateCGImagesAsynchronouslyForTimes:requestedTimes completionHandler:^(CMTime requestedTime, CGImageRef  _Nullable image, CMTime actualTime, AVAssetImageGeneratorResult result, NSError * _Nullable error) {
        
        if (AVAssetImageGeneratorSucceeded == result) {
            if (image) {
                dispatch_async(dispatch_get_main_queue(), ^{
                    UIImage *result_image = [[UIImage alloc] initWithCGImage:image];
                                        
                    complete(result_image,nil);
                    //需要釋放
                    CGImageRelease(image);
                });
            }
            //展示時間
            CMTimeShow(requestedTime);
            CMTimeShow(actualTime);

        }else{
            dispatch_async(dispatch_get_main_queue(), ^{
                complete(nil,error);
            });
        }

    }];
}

首先我們需要用到的類為AVAssetImageGenerator
AVAssetImageGenerator: 是用來提供視頻的縮略圖或預(yù)覽視頻的幀的類.
通過該類,我們可以獲取視頻的所有幀,那么在獲取你想要的幀,那肯定不是什么難事了。
這里有幾個參數(shù)
requestedTimeToleranceAfterrequestedTimeToleranceBefore:應(yīng)該是指截圖圖片幀真實時間的一個浮動值,當然為了達到我們想要的時間,建議設(shè)置為0.
maximumSize:設(shè)置圖片的尺寸
在設(shè)置后完成后,就可以直接調(diào)用函數(shù)

- (void)generateCGImagesAsynchronouslyForTimes:(NSArray<NSValue *> *)requestedTimes completionHandler:(AVAssetImageGeneratorCompletionHandler)handler;
圖片合成為視頻
先上個圖
樣圖.jpg

我們先來了解下AVAssetWriter類:
AVAssetWriter類將媒體數(shù)據(jù)從多個源寫入指定文件格式的單個文件。不需要將asset writer對象與特定的asset相關(guān)聯(lián),但必須為要創(chuàng)建的每個輸出文件使用單獨的asset writer。由于asset writer可以從多個源寫入媒體數(shù)據(jù),因此必須要為寫入文件的每個track創(chuàng)建一個AVAssetWriterInput對象,每個AVAssetWriterInput期望以CMSampleBufferRef對象形式接收數(shù)據(jù),但如果你想要將CVPixelBufferRef類型對象添加到asset writer input,就需要使用AVAssetWriterInputPixelBufferAdaptor類。

AVAssetWriter用于對媒體資源進行編碼并將其寫入到容器文件中,比如一個MPEG-4文件或QuickTime文件。它由一個或多個AVAssetWriterInput對象配置,用于附加將包含要寫入容器的媒體樣本的CMSampleBufferRef對象。AVAssetWriterInput被配置為可以處理指定的媒體類型,比如音頻或視頻,并且附加在其后的樣本會在最終輸出時生成一個獨立的AVAssetTrack。當使用一個配置了處理視頻樣本AVAssetWriterInput時,開發(fā)者會經(jīng)常用到一個專門的適配器對象AVAssetWriterInputPixelBufferAdaptor。這個類在附加被包裝為CVPixelBufferRef對象的視頻樣本是提供最優(yōu)性能。輸入信息也可以通過使用AVAssetWriterInputGroup組成互斥的參數(shù)。這就讓開發(fā)者能夠創(chuàng)建特定資源,其中包含在播放時使用AVMediaSelectionGroupAVMediaSelectionOption類選擇的指定語言媒體軌道。

注意:與上面我們用到的AVAssetExportSession相比,AVAssetWriter明顯的優(yōu)勢就是它對輸出進行編碼時能夠進行更加細致的壓縮設(shè)置控制??梢宰岄_發(fā)者指定諸如關(guān)鍵幀間隔、視頻比特率、H.264配置文件、像素寬高比和純凈光圈等設(shè)置

在有了上面的了解之后,我們就可以動手了。
1、創(chuàng)建AVAssetWriter對象

    AVAssetWriter *assetWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:outPutFilePath] fileType:AVFileTypeQuickTimeMovie error:&outError];

2、設(shè)置輸出視頻相關(guān)信息

    //視頻尺寸
    CGSize size = CGSizeMake(320, 480);
    //meaning that it must contain AVVideoCodecKey, AVVideoWidthKey, and AVVideoHeightKey.
    //視頻信息設(shè)置
    NSDictionary *outPutSettingDic = [NSDictionary dictionaryWithObjectsAndKeys:
                                      AVVideoCodecTypeH264,AVVideoCodecKey,
                                      [NSNumber numberWithInt:size.width],AVVideoWidthKey,
                                      [NSNumber numberWithInt:size.height],AVVideoHeightKey, nil];

    AVAssetWriterInput *videoWriterInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:outPutSettingDic];

3、設(shè)置輸入信息

        NSDictionary *sourcePixelBufferAttributes = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA],kCVPixelBufferPixelFormatTypeKey, nil];
        
        AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput sourcePixelBufferAttributes:sourcePixelBufferAttributes];

由于我們是圖片合成為視頻,所以這里采用的是CVPixelBufferRef添加到asset writer input,故使用AVAssetWriterInputPixelBufferAdaptor.
4、進行資源寫入合成視頻

        //開一個隊列
        dispatch_queue_t dispatchQueue = dispatch_queue_create("mediaQueue", NULL);
        NSInteger  __block index = 0;
        
        [videoWriterInput requestMediaDataWhenReadyOnQueue:dispatchQueue usingBlock:^{
            while ([videoWriterInput isReadyForMoreMediaData])
            {
                if (++index >= images.count * 30) {
                    [videoWriterInput markAsFinished];
                    [assetWriter finishWritingWithCompletionHandler:^{
                        dispatch_async(dispatch_get_main_queue(), ^{
                            successBlcok([NSURL fileURLWithPath:outPutFilePath]);
                        });
                    }];
                    break;
                }
                long idx = index / 30;
                NSLog(@" 打印信息:%ld",idx);
                //先將圖片轉(zhuǎn)換成CVPixelBufferRef
                UIImage *image = images[idx];
                CVPixelBufferRef pixelBuffer = [image pixelBufferRefWithSize:size];
                if (pixelBuffer) {
                    CMTime time = CMTimeMake(index, 30);
                    if ([adaptor appendPixelBuffer:pixelBuffer withPresentationTime:time]) {
                    
                        NSLog(@"OK++%f",CMTimeGetSeconds(time));
                    }else{
                        NSLog(@"Fail");
                    }
                    CFRelease(pixelBuffer);
                }
            }
        }];

在上述代碼中,有這么個函數(shù)

- (BOOL)appendPixelBuffer:(CVPixelBufferRef)pixelBuffer withPresentationTime:(CMTime)presentationTime

該函數(shù)的功能就是添加幀,presentationTime代表輸出文件中幀的時間,可以根據(jù)自己的需求來設(shè)置該事件。
注:在完成合成后,需要在主線程中進行其它操作。

將圖片合成為視頻 并加上音樂

先上代碼

- (void)compositionVideoWithImage:(NSArray<UIImage *> *)images videoName:(NSString *)videoName audio:(NSURL *)audioUrl success:(SuccessBlcok)successBlcok
{
    if (!audioUrl) {
        [self compositionVideoWithImage:images videoName:videoName success:successBlcok];
    }else{
        NSCAssert(videoName.length > 0, @"請輸入轉(zhuǎn)換后的名字");
        NSString *outPutFilePath = [[self videoPath] stringByAppendingPathComponent:videoName];
        
        // 音頻采集
        AVURLAsset *audioAsset = [[AVURLAsset alloc] initWithURL:audioUrl options:nil];
        
        NSString *serializationQueueDescription = [NSString stringWithFormat:@"%@ serialization queue", self];
        
        // Create the main serialization queue.
        dispatch_queue_t mainSerializationQueue = dispatch_queue_create([serializationQueueDescription UTF8String], NULL);
        _m_queue = mainSerializationQueue;

        [audioAsset loadValuesAsynchronouslyForKeys:@[@"tracks"] completionHandler:^{
            dispatch_async(mainSerializationQueue, ^{
                BOOL success = YES;
                NSError *localError = nil;
                // Check for success of loading the assets tracks.
                success = ([audioAsset statusOfValueForKey:@"tracks" error:&localError] == AVKeyValueStatusLoaded);
                
                // If the tracks loaded successfully, make sure that no file exists at the output path for the asset writer.
                if (success) {
                    //存在該文件
                    if ([GLFolderManager fileExistsAtPath:outPutFilePath]) {
                        [GLFolderManager clearCachesWithFilePath:outPutFilePath];
                    }
                    
                    AVAssetTrack *assetAudioTrack = nil;
                    NSArray *audioTracks = [audioAsset tracksWithMediaType:AVMediaTypeAudio];
                    if (audioTracks.count > 0) {
                        assetAudioTrack = [audioTracks objectAtIndex:0];
                    }
                    
                    //音頻通道是否存在
                    if (assetAudioTrack)
                    {
                        NSError *error;
                        //創(chuàng)建讀出
                        AVAssetReader *assetReader = [[AVAssetReader alloc] initWithAsset:audioAsset error:&error];
                        BOOL success = (assetReader != nil);
                        
                        AVAssetWriter *assetWriter = nil;
                        //創(chuàng)建寫入
                        if (success) {
                            NSError *outError;
                            assetWriter = [[AVAssetWriter alloc] initWithURL:[NSURL fileURLWithPath:outPutFilePath] fileType:AVFileTypeQuickTimeMovie error:&outError];
                            success = (assetWriter != nil);
                        }
                        if (success) {
                            //將track里的數(shù)據(jù) 讀取出來
                            // If there is an audio track to read, set the decompression settings to Linear PCM and create the asset reader output.
                            NSDictionary *decompressionAudioSettings = @{AVFormatIDKey : [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM]};
                            AVAssetReaderTrackOutput *assetReaderAudioOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:assetAudioTrack outputSettings:decompressionAudioSettings];
                            if ([assetReader canAddOutput:assetReaderAudioOutput]) {
                                [assetReader addOutput:assetReaderAudioOutput];
                            }
                            
                            // Then, set the compression settings to 128kbps AAC and create the asset writer input.
                            AudioChannelLayout stereoChannelLayout = {
                                .mChannelLayoutTag = kAudioChannelLayoutTag_Stereo,
                                .mChannelBitmap = 0,
                                .mNumberChannelDescriptions = 0
                            };
                            NSData *channelLayoutAsData = [NSData dataWithBytes:&stereoChannelLayout length:offsetof(AudioChannelLayout, mChannelDescriptions)];
                            NSDictionary *compressionAudioSettings = @{
                                                                       AVFormatIDKey         : [NSNumber numberWithUnsignedInt:kAudioFormatMPEG4AAC],
                                                                       AVEncoderBitRateKey   : [NSNumber numberWithInteger:128000],
                                                                       AVSampleRateKey       : [NSNumber numberWithInteger:44100],
                                                                       AVChannelLayoutKey    : channelLayoutAsData,
                                                                       AVNumberOfChannelsKey : [NSNumber numberWithUnsignedInteger:2]
                                                                       };
                            
                            //將讀取的內(nèi)容寫入
                            AVAssetWriterInput *assetWriterAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:[assetAudioTrack mediaType] outputSettings:compressionAudioSettings];
                            
                            if ([assetWriter canAddInput:assetWriterAudioInput]) {
                                [assetWriter addInput:assetWriterAudioInput];
                            }
                            
                            
                            //--------------圖片寫入視頻設(shè)置
                            //視頻尺寸
                            CGSize size = CGSizeMake(320, 480);
                            //meaning that it must contain AVVideoCodecKey, AVVideoWidthKey, and AVVideoHeightKey.
                            //視頻信息設(shè)置
                            NSDictionary *outPutSettingDic = [NSDictionary dictionaryWithObjectsAndKeys:
                                                              AVVideoCodecTypeH264,AVVideoCodecKey,
                                                              [NSNumber numberWithInt:size.width],AVVideoWidthKey,
                                                              [NSNumber numberWithInt:size.height],AVVideoHeightKey, nil];
                            
                            AVAssetWriterInput *videoWriterInput = [[AVAssetWriterInput alloc] initWithMediaType:AVMediaTypeVideo outputSettings:outPutSettingDic];
                            //每個AVAssetWriterInput期望以CMSampleBufferRef對象形式接收數(shù)據(jù),但如果你想要將CVPixelBufferRef類型對象添加到assetwriterinput,就使用AVAssetWriterInputPixelBufferAdaptor類。
                            
                            //像素緩沖區(qū)屬性,這些屬性最接近于被附加的視頻幀的源格式。
                            //To specify the pixel format type, the pixelBufferAttributes dictionary should contain a value for kCVPixelBufferPixelFormatTypeKey
                            NSDictionary *sourcePixelBufferAttributes = [NSDictionary dictionaryWithObjectsAndKeys:[NSNumber numberWithInt:kCVPixelFormatType_32BGRA],kCVPixelBufferPixelFormatTypeKey, nil];
                            
                            AVAssetWriterInputPixelBufferAdaptor *adaptor = [AVAssetWriterInputPixelBufferAdaptor assetWriterInputPixelBufferAdaptorWithAssetWriterInput:videoWriterInput sourcePixelBufferAttributes:sourcePixelBufferAttributes];
                            
                            if ([assetWriter canAddInput:videoWriterInput]) {
                                [assetWriter addInput:videoWriterInput];
                            }
                            
                            //--------上面為設(shè)置部分  下面為開始執(zhí)行讀取和寫入
                            
                            BOOL readSuccess = YES;
                            BOOL writeSuccess = YES;
                            readSuccess = [assetReader startReading];
                            if (!readSuccess) {
                                NSError *readError = assetReader.error;
                                NSLog(@" 打印信息:%@",readError);
                            }
                            if (readSuccess) {
                                writeSuccess = [assetWriter startWriting];
                            }
                            
                            if (!writeSuccess)
                            {
                                NSError *writeError = [assetWriter error];
                                NSLog(@" 打印信息:%@",writeError);
                            }
                            if (writeSuccess)
                            {
                                NSLog(@" 打印信息:+++++");
                                BOOL __block audioFinished = NO;
                                BOOL __block videoFinished = NO;
                                
                                // If the asset reader and writer both started successfully, create the dispatch group where the reencoding will take place and start a sample-writing session.
                                dispatch_group_t dispatchGroup = dispatch_group_create();
                                [assetWriter startSessionAtSourceTime:kCMTimeZero];
                                
                                if (assetWriterAudioInput) {
                                    //加入第一個任務(wù)
                                    // If there is audio to reencode, enter the dispatch group before beginning the work.
                                    dispatch_group_enter(dispatchGroup);
                                    
                                    //開一個隊列
                                    dispatch_queue_t dispatchQueue = dispatch_queue_create("mediaAudioQueue", NULL);
                                    
                                    [assetWriterAudioInput requestMediaDataWhenReadyOnQueue:dispatchQueue usingBlock:^{
                                        // Because the block is called asynchronously, check to see whether its task is complete.
                                        if (audioFinished) {
                                            return ;
                                        }
                                        BOOL completedOrFailed = NO;
                                        while ([assetWriterAudioInput isReadyForMoreMediaData] && !completedOrFailed)
                                        {
                                            // Get the next audio sample buffer, and append it to the output file.
                                            CMSampleBufferRef sampleBuffer = [assetReaderAudioOutput copyNextSampleBuffer];
                                            
                                            //特別備注。。。找了幾百年的bug  居然是 必須要引用assetReader.status 否則會crash
                                            if ((assetReader.status == AVAssetReaderStatusReading) && sampleBuffer != NULL)
                                            {
                                                BOOL success = [assetWriterAudioInput appendSampleBuffer:sampleBuffer];
                                                CFRelease(sampleBuffer);
                                                sampleBuffer = NULL;
                                                completedOrFailed = !success;
                                            }
                                            else
                                            {
                                                if (assetReader.status == AVAssetReaderStatusCompleted) {
                                                    completedOrFailed = YES;
                                                }else{
                                                    NSLog(@" 打印信息:%@",assetReader.error);
                                                    completedOrFailed = YES;
                                                }
                                            }
                                            
                                            
                                        }
                                        
                                        if (completedOrFailed)
                                        {
                                            // Mark the input as finished, but only if we haven't already done so, and then leave the dispatch group (since the audio work has finished).
                                            BOOL oldFinished = audioFinished;
                                            audioFinished = YES;
                                            if (oldFinished == NO)
                                            {
                                                [assetWriterAudioInput markAsFinished];
                                            }
                                            //和dispatch_group_enter成對出現(xiàn) 出隊列
                                            dispatch_group_leave(dispatchGroup);
                                        }
                                    }];
                                }
                                
                                if (videoWriterInput) {
                                    dispatch_group_enter(dispatchGroup);
                                    dispatch_queue_t dispatchQueue = dispatch_queue_create("mediaQueue", NULL);
                                    NSInteger  __block index = 0;
                                    [videoWriterInput requestMediaDataWhenReadyOnQueue:dispatchQueue usingBlock:^{
                                        if (videoFinished) {
                                            return;
                                        }
                                        while ([videoWriterInput isReadyForMoreMediaData])
                                        {
                                            if (++index >= images.count * 100) {
                                                [videoWriterInput markAsFinished];
                                                dispatch_group_leave(dispatchGroup);
                                                break;
                                            }
                                            long idx = index / 100;
                                            //先將圖片轉(zhuǎn)換成CVPixelBufferRef
                                            UIImage *image = images[idx];
                                            CVPixelBufferRef pixelBuffer = [image pixelBufferRefWithSize:size];
                                            if (pixelBuffer) {
                                                CMTime time = CMTimeMake(index, 30);
                                                if ([adaptor appendPixelBuffer:pixelBuffer withPresentationTime:time]) {
//                                                    NSLog(@"OK++%f",CMTimeGetSeconds(time));
                                                }else{
                                                    NSLog(@"Fail");
                                                }
                                                CFRelease(pixelBuffer);
                                            }
                                        }
                                    }];
                                }

                                dispatch_notify(dispatchGroup,mainSerializationQueue, ^{
                                        NSLog(@" 打印信息:+++++++");
                                    [assetWriter finishWritingWithCompletionHandler:^{
                                        dispatch_async(dispatch_get_main_queue(), ^{
                                            successBlcok([NSURL fileURLWithPath:outPutFilePath]);
                                        });
                                    }];
                                });
                            }
                        }
                    }
                }
            });
        }];
    }
}

這個代碼有點長,下面讓我們一點點來了解
1、由于這已經(jīng)不是單純的視頻音頻的合并,而是涉及通過寫入的方式來輸出多媒體,所以我們不能再用最開始的提取音視頻通道來進行簡單的合并,而是需要通過寫入的方式來進行
2、圖片合成視頻我們已經(jīng)知道方法,如果需要將音頻也寫入,那么我們首先要將音頻讀出,所以就有了下面的方法
3、音頻采集

        // 音頻采集
        AVURLAsset *audioAsset = [[AVURLAsset alloc] initWithURL:audioUrl options:nil];

4、由于多媒體文件一般比較大,獲取或計算出Asset中的屬性非常耗時,appleAsset的屬性采用了懶惰加載模式。在創(chuàng)建AVAsset的時候,只生成一個實例,并不初始化屬性。只有當?shù)谝淮卧L問屬性時,系統(tǒng)才會根據(jù)多媒體中的數(shù)據(jù)初始化這個屬性。
由于不用同時加載所有屬性,耗時問題得到了一定緩解。但是屬性加載在計算量比較大的時候仍舊可能會阻塞線程。為了解決這個問題,AVFoundation提供了AVAsynchronousKeyValueLoading協(xié)議,可以異步加載屬性:

- (void)loadValuesAsynchronouslyForKeys:(NSArray<NSString *> *)keys completionHandler:(nullable void (^)(void))handler;

這里我們只需要知道tracks屬性,所以

[audioAsset loadValuesAsynchronouslyForKeys:@[@"tracks"] completionHandler:^{

}];

5、將track里的數(shù)據(jù)讀取出來,先進行設(shè)置

       NSDictionary *decompressionAudioSettings = @{AVFormatIDKey : [NSNumber numberWithUnsignedInt:kAudioFormatLinearPCM]};
       AVAssetReaderTrackOutput *assetReaderAudioOutput = [AVAssetReaderTrackOutput assetReaderTrackOutputWithTrack:assetAudioTrack outputSettings:decompressionAudioSettings];
                            if ([assetReader canAddOutput:assetReaderAudioOutput]) {
                                [assetReader addOutput:assetReaderAudioOutput];
                            }

6、設(shè)置輸入信息,并寫入音頻,先進行設(shè)置

                       // Then, set the compression settings to 128kbps AAC and create the asset writer input.
                            AudioChannelLayout stereoChannelLayout = {
                                .mChannelLayoutTag = kAudioChannelLayoutTag_Stereo,
                                .mChannelBitmap = 0,
                                .mNumberChannelDescriptions = 0
                            };
                            NSData *channelLayoutAsData = [NSData dataWithBytes:&stereoChannelLayout length:offsetof(AudioChannelLayout, mChannelDescriptions)];
                            NSDictionary *compressionAudioSettings = @{
                                                                       AVFormatIDKey         : [NSNumber numberWithUnsignedInt:kAudioFormatMPEG4AAC],
                                                                       AVEncoderBitRateKey   : [NSNumber numberWithInteger:128000],
                                                                       AVSampleRateKey       : [NSNumber numberWithInteger:44100],
                                                                       AVChannelLayoutKey    : channelLayoutAsData,
                                                                       AVNumberOfChannelsKey : [NSNumber numberWithUnsignedInteger:2]
                                                                       };
                            
                            //將讀取的內(nèi)容寫入
                            AVAssetWriterInput *assetWriterAudioInput = [AVAssetWriterInput assetWriterInputWithMediaType:[assetAudioTrack mediaType] outputSettings:compressionAudioSettings];
                            
                            if ([assetWriter canAddInput:assetWriterAudioInput]) {
                                [assetWriter addInput:assetWriterAudioInput];
                            }

7、視頻輸入信息設(shè)置,不再累贅說明,代碼中有詳細說明
8、在設(shè)置完上訴信息后,我們就該進行讀取和寫入了,為了提高合成效率,這里我們采用異步并行執(zhí)行,為了保證再兩個輸入都完成,這里采用了線程組的方式dispatch_group_t,大體步驟如下

dispatch_group_t dispatchGroup = dispatch_group_create();
//加入音頻寫入
dispatch_group_enter(dispatchGroup);
...
//音頻寫入操作
...
//和dispatch_group_enter成對出現(xiàn) 出隊列
dispatch_group_leave(dispatchGroup);

//加入視頻寫入
dispatch_group_enter(dispatchGroup);
...
//視頻寫入操作
...
//和dispatch_group_enter成對出現(xiàn) 出隊列
dispatch_group_leave(dispatchGroup);


//獲取結(jié)果 得到輸出地址 記得在主線程中進行操作
    dispatch_notify(dispatchGroup,mainSerializationQueue, ^{
        NSLog(@" 打印信息:+++++++");
        [assetWriter finishWritingWithCompletionHandler:^{
            dispatch_async(dispatch_get_main_queue(), ^{
                successBlcok([NSURL fileURLWithPath:outPutFilePath]);
            });
        }];
    });

在上述音頻讀取中,用到了

CMSampleBufferRef sampleBuffer = [assetReaderAudioOutput copyNextSampleBuffer];

API中是這么描述的
Copies the next sample buffer for the output synchronously.大概意思就是:同步復(fù)制輸出的下一個示例緩沖區(qū),即得到我們想要的數(shù)據(jù)

視頻水印處理

在水印處理之前,大家可以先看看文章上面我的手繪圖,有這么幾個類:
AVMutableVideoComposition:用來生成video的組合指令,包含多段instruction。可以決定最終視頻的尺寸,裁剪需要在這里進行;
AVMutableVideoCompositionInstruction:一個指令,決定一個timeRange內(nèi)每個軌道的狀態(tài),包含多個layerInstruction
AVMutableVideoCompositionLayerInstruction:在一個指令的時間范圍內(nèi),某個軌道的狀態(tài);

先看看水印的核心代碼

    //創(chuàng)建合成指令
    AVMutableVideoCompositionInstruction *videoCompostionInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];
    //設(shè)置時間范圍
    videoCompostionInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, videoAssetTrack.timeRange.duration);
    //創(chuàng)建層指令,并將其與合成視頻軌道相關(guān)聯(lián)
    AVMutableVideoCompositionLayerInstruction *videoLayerInstruction = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoCompositionTrack];

    [videoLayerInstruction setTransform:videoAssetTrack.preferredTransform atTime:kCMTimeZero];
    [videoLayerInstruction setOpacity:0.0 atTime:videoAssetTrack.timeRange.duration];
    videoCompostionInstruction.layerInstructions = @[videoLayerInstruction];
    
    
    BOOL isVideoAssetPortrait_  = NO;
    CGAffineTransform videoTransform = videoAssetTrack.preferredTransform;
    if (videoTransform.a == 0 && videoTransform.b == 1.0 && videoTransform.c == -1.0 && videoTransform.d == 0) {
        //        videoAssetOrientation_ = UIImageOrientationRight;
        isVideoAssetPortrait_ = YES;
    }
    if (videoTransform.a == 0 && videoTransform.b == -1.0 && videoTransform.c == 1.0 && videoTransform.d == 0) {
        //        videoAssetOrientation_ =  UIImageOrientationLeft;
        isVideoAssetPortrait_ = YES;
    }
    CGSize naturalSize;
    if(isVideoAssetPortrait_){
        naturalSize = CGSizeMake(videoAssetTrack.naturalSize.height, videoAssetTrack.naturalSize.width);
    } else {
        naturalSize = videoAssetTrack.naturalSize;
    }
    
    //創(chuàng)建視頻組合
    //Attach the video composition instructions to the video composition
    AVMutableVideoComposition *mutableVideoComposition = [AVMutableVideoComposition videoComposition];
    //必須設(shè)置 下面的尺寸和時間
    mutableVideoComposition.renderSize = naturalSize;
    mutableVideoComposition.frameDuration = CMTimeMake(1, 25);//videoAssetTrack.timeRange.duration;
    mutableVideoComposition.instructions = @[videoCompostionInstruction];
    [self addWaterLayerWithAVMutableVideoComposition:mutableVideoComposition];

++++

- (void)addWaterLayerWithAVMutableVideoComposition:(AVMutableVideoComposition*)mutableVideoComposition
{
    //-------------------layer
    CALayer *watermarkLayer = [CALayer layer];
    [watermarkLayer setContents:(id)[UIImage imageNamed:@"白兔"].CGImage];
    watermarkLayer.bounds = CGRectMake(0, 0, 130, 130);
    watermarkLayer.position = CGPointMake(mutableVideoComposition.renderSize.width/2, mutableVideoComposition.renderSize.height/4);
    
    CALayer *sheepLayer = [CALayer layer];
    [sheepLayer setContents:(id)[UIImage imageNamed:@"綿陽"].CGImage];
    sheepLayer.bounds = CGRectMake(0, 0, 130, 130);
    sheepLayer.position = CGPointMake(mutableVideoComposition.renderSize.width/2, mutableVideoComposition.renderSize.height - 150);
    
    
    CALayer *mouseLayer = [CALayer layer];
    [mouseLayer setContents:(id)[UIImage imageNamed:@"老鼠"].CGImage];
    mouseLayer.bounds = CGRectMake(0, 0, 130, 130);
    mouseLayer.position = CGPointMake(mutableVideoComposition.renderSize.width/2, mutableVideoComposition.renderSize.height/2);

    
    CALayer *parentLayer = [CALayer layer];
    CALayer *videoLayer = [CALayer layer];
    
    parentLayer.frame = CGRectMake(0, 0, mutableVideoComposition.renderSize.width, mutableVideoComposition.renderSize.height);
    videoLayer.frame = CGRectMake(0, 0, mutableVideoComposition.renderSize.width, mutableVideoComposition.renderSize.height);

    [parentLayer addSublayer:videoLayer];
    
    [parentLayer addSublayer:watermarkLayer];
    [parentLayer addSublayer:sheepLayer];
    [parentLayer addSublayer:mouseLayer];
    
    
    //添加文字
    UIFont *font = [UIFont systemFontOfSize:30.0];
    NSString *text = @"加點水印看看效果";
    CATextLayer *textLayer = [[CATextLayer alloc] init];
    [textLayer setFontSize:30];
    [textLayer setString:text];
    [textLayer setAlignmentMode:kCAAlignmentLeft];
    [textLayer setForegroundColor:[[UIColor whiteColor] CGColor]];
    
    CGSize textSize = [text sizeWithAttributes:[NSDictionary dictionaryWithObjectsAndKeys:font,NSFontAttributeName, nil]];
    CGFloat textH = textSize.height + 10;
    [textLayer setFrame:CGRectMake(100, mutableVideoComposition.renderSize.height-100, textSize.width + 20, textH)];
    
    [parentLayer addSublayer:textLayer];
    
    mutableVideoComposition.animationTool = [AVVideoCompositionCoreAnimationTool videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:parentLayer];
    
    CABasicAnimation *rotationAnima = [CABasicAnimation animationWithKeyPath:@"transform.rotation.z"];
    rotationAnima.fromValue = @(0);
    rotationAnima.toValue = @(-M_PI * 2);
    rotationAnima.repeatCount = HUGE_VALF;
    rotationAnima.duration = 2.0f;  //5s之后消失
    [rotationAnima setRemovedOnCompletion:NO];
    [rotationAnima setFillMode:kCAFillModeForwards];
    rotationAnima.beginTime = AVCoreAnimationBeginTimeAtZero;
    [watermarkLayer addAnimation:rotationAnima forKey:@"Aniamtion"];
    
    
    CGPoint mousePoint = mouseLayer.position;
    CAKeyframeAnimation *animation = [CAKeyframeAnimation animationWithKeyPath:@"position"];
    NSValue *key1 = [NSValue valueWithCGPoint:mouseLayer.position];
    NSValue *key2 = [NSValue valueWithCGPoint:CGPointMake(mousePoint.x + 60, mousePoint.y + 40)];
    NSValue *key3 = [NSValue valueWithCGPoint:CGPointMake(mousePoint.x + 120, mousePoint.y)];
    NSValue *key4 = [NSValue valueWithCGPoint:CGPointMake(mousePoint.x + 120, mousePoint.y)];
    animation.values = @[key1,key2,key3,key4];
    animation.duration = 5.0;
    animation.autoreverses = true;//是否按路徑返回
    animation.repeatCount = HUGE_VALF;//是否重復(fù)執(zhí)行
    animation.removedOnCompletion = NO;//執(zhí)行后移除動畫
    animation.fillMode = kCAFillModeForwards;
    animation.beginTime = AVCoreAnimationBeginTimeAtZero;
    [mouseLayer addAnimation:animation forKey:@"keyframeAnimation_fish"];
}

其中

    mutableVideoComposition.animationTool = [AVVideoCompositionCoreAnimationTool videoCompositionCoreAnimationToolWithPostProcessingAsVideoLayer:videoLayer inLayer:parentLayer];

則是設(shè)置水印的關(guān)鍵一步,這邊將我們想要設(shè)置的水印layer添加到我們想要的視頻中,其他關(guān)于AVMutableVideoCompositionInstructionAVMutableVideoCompositionLayerInstruction的設(shè)置均是對視頻的一些設(shè)置,可以有很多設(shè)置,大家有時間可以去了解下,我這里只是簡單的設(shè)置了下。

尾章

終于一口氣寫完,講述的不是很詳細,還請各位看官見諒!請忽略那個彈鋼琴的視頻...辣眼睛
下面還是附上傳送門
由于GitHub上傳文件的限制,導(dǎo)致工程中有幾個視頻和音頻不能傳上去,所以這里只好通過網(wǎng)盤 密碼:mygj
將下載下來的資源放到這里面就ok了

23.png

參考文章:官方文檔

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容