人而無信不知其可
前言
很久很久沒有寫點什么了,只因為最近事情太多了,這幾天終于閑下來了,趁此機會,記錄下幾個月前寫的一個關(guān)于視頻音頻圖片合成方面的一個小例子
入場
先來看看實現(xiàn)的大概功能吧~

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

下面就讓我們一點點來分析分析
需要了解什么
先來看一個關(guān)系圖,字寫的丑,將就著看吧....
看著上面的圖,是有點凌亂的感覺,下面我們就一點點來剝開。
代碼實現(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ù)
requestedTimeToleranceAfter和requestedTimeToleranceBefore:應(yīng)該是指截圖圖片幀真實時間的一個浮動值,當然為了達到我們想要的時間,建議設(shè)置為0.
maximumSize:設(shè)置圖片的尺寸
在設(shè)置后完成后,就可以直接調(diào)用函數(shù)
- (void)generateCGImagesAsynchronouslyForTimes:(NSArray<NSValue *> *)requestedTimes completionHandler:(AVAssetImageGeneratorCompletionHandler)handler;
圖片合成為視頻
先上個圖

我們先來了解下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)建特定資源,其中包含在播放時使用AVMediaSelectionGroup和AVMediaSelectionOption類選擇的指定語言媒體軌道。
注意:與上面我們用到的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中的屬性非常耗時,apple對Asset的屬性采用了懶惰加載模式。在創(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)于AVMutableVideoCompositionInstruction和AVMutableVideoCompositionLayerInstruction的設(shè)置均是對視頻的一些設(shè)置,可以有很多設(shè)置,大家有時間可以去了解下,我這里只是簡單的設(shè)置了下。
尾章
終于一口氣寫完,講述的不是很詳細,還請各位看官見諒!請忽略那個彈鋼琴的視頻...辣眼睛
下面還是附上傳送門
由于GitHub上傳文件的限制,導(dǎo)致工程中有幾個視頻和音頻不能傳上去,所以這里只好通過網(wǎng)盤 密碼:mygj
將下載下來的資源放到這里面就ok了

參考文章:官方文檔