iOS 给视频添加水印 动画 背景音乐

视频处理主要是用到以下这几个类

AVMutableComposition、AVMutableVideoComposition、AVMutableAudioMix、AVMutableVideoCompositionInstruction、AVMutableVideoCompositionLayerInstruction、AVAssetExportSession 等。其中 AVMutableComposition 可以用来操作音频和视频的组合,AVMutableVideoComposition 可以用来对视频进行操作,AVMutableAudioMix 类是给视频添加音频的,AVMutableVideoCompositionInstruction和AVMutableVideoCompositionLayerInstruction 一般都是配合使用,用来给视频添加水印或者旋转视频方向,AVAssetExportSession 是用来进行视频导出操作的。需要值得注意的是当App进入后台之后,会对使用到GPU的代码操作进行限制,会造成崩溃,而视频处理这些功能多数会使用到GPU,所以需要做对应的防错处理。

在这里我会使用Apple的官方Demo “AVSimpleEditoriOS” 作为讲解案例,该案例采用Command设计模式来组织代码,其中基类的AVSECommand包含了一些各个子类Command共用的属性。本文就视频相关操作做简要介绍,说明一些相关的操作,并标注一些重点代码,希望本文可以起到抛砖引玉的效果,让大家对视频剪辑处理有个初步印象,然后可以根据Apple官方Demo的内容进行相应的修改。大家可以下载相应的Apple官方Demo运行查看结果。

第一节:给视频添加水印和背景边框

今天第一节先讲解如何为一个视频添加边框和动画,首先说明的是,这种边框和动画并不能直接修改视频的某一帧给他增加边框或者产生动画效果,这种动画更像是给视频的上面加一个calayer,然后控制这个layer产生动画效果。因为具体到某一帧的这种操作不是iphone应该做的他也做不到。

我们先来看一张图,了解一下给video增加动画的原理。


你可以看到videoLayer这个东西,其实这个layer就是负责显示我们的视频,和他同级的是一个叫animationLayer的东西,我们能够掌控并且玩出花样的其实是这个叫animationLayer的东西,因为这个animationLayer可以由我们自己创建。

其实很简单,和我们videoLayer同级别的layer叫animationLayer(就是background),他们共同有个父类叫做parentLayer,那么增加边框无非是把animationLayer这个layer找个边框的图片,然后把他放到videoLayer的下面,然后把videoLayer(crop也就是裁剪)的尺寸控制到刚好能显示animationLayer的四边,这样,不就成了带边框的效果么。简单点讲就是 把裁剪好的视频 videoLayer放在了 背景上然后再加入到parentLayer里 。

视频添加水印和背景边框具体步骤

1.拿到视频和音频资源

2.创建AVMutableComposition对象

3.往AVMutableComposition对象添加视频资源,同时设置视频资源的时间段和插入点

4.往AVMutableComposition对象添加音频资源,同时设置音频资源的时间段和插入点

5.创建视频组合器对象 AVMutableVideoComposition 并设置frame和渲染宽高

6.创建视频组合器指令对象,设置指令的作用范围

7.创建视频组合器图层指令对象,设置指令的作用范围

8.视频组合器图层指令对象 放入 视频组合器指令对象中

9.视频组合器指令对象放入视频组合器对象

10.创建水印图层Layer并设置frame和水印的位置,并将水印加入视频组合器中

具体代码实现

- (void)performWithAsset:(AVAsset*)asset withImageNamed:(NSString*)imgName withColorName:(NSString*)color withMusicName:(NSString *)musicName with:(CGRect)photoSize{

CGSize videoSize;

// 获取视频资源和音频资源

AVAssetTrack *assetVideoTrack = nil;

AVAssetTrack *assetAudioTrack = nil;

// Check if the asset contains video and audio tracks

if ([[asset tracksWithMediaType:AVMediaTypeVideo] count] != 0) {

assetVideoTrack = [asset tracksWithMediaType:AVMediaTypeVideo][0];

}

if ([[asset tracksWithMediaType:AVMediaTypeAudio] count] != 0) {

assetAudioTrack = [asset tracksWithMediaType:AVMediaTypeAudio][0];

}

CMTime insertionPoint = kCMTimeZero;

NSError *error = nil;

// 创建组合器对象并添加视频资源和音频资源

// Step 1

// Create a composition with the given asset and insert audio and video tracks into it from the asset

if(!self.mutableComposition) {

// Check if a composition already exists, else create a composition using the input asset

self.mutableComposition = [AVMutableComposition composition];

// Insert the video and audio tracks from AVAsset

if (assetVideoTrack != nil) {

AVMutableCompositionTrack *compositionVideoTrack = [self.mutableComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];

[compositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, [asset duration]) ofTrack:assetVideoTrack atTime:insertionPoint error:&error];

}

if (assetAudioTrack != nil) {

AVMutableCompositionTrack *compositionAudioTrack = [self.mutableComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];

[compositionAudioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, [asset duration]) ofTrack:assetAudioTrack atTime:insertionPoint error:&error];

}

}

// Step 2  创建一个和视频相同大小的背景层

if ([[self.mutableComposition tracksWithMediaType:AVMediaTypeVideo] count] != 0) {

if(!self.mutableVideoComposition) {

self.mutableVideoComposition = [AVMutableVideoComposition videoComposition];

AVMutableVideoCompositionInstruction *passThroughInstruction = [AVMutableVideoCompositionInstruction videoCompositionInstruction];

AVAssetTrack *videoTrack = [self.mutableComposition tracksWithMediaType:AVMediaTypeVideo][0];

AVMutableVideoCompositionLayerInstruction *passThroughLayer = [AVMutableVideoCompositionLayerInstruction videoCompositionLayerInstructionWithAssetTrack:videoTrack];

//对视频大小进行裁剪,我这里是将视频裁剪成 等宽高的正方形

CGFloat rate;

CGSize renderSize = CGSizeMake(0, 0);

renderSize.width = MAX(renderSize.width, videoTrack.naturalSize.height);

renderSize.height = MAX(renderSize.height, videoTrack.naturalSize.width);

CGFloat renderW = MIN(renderSize.width, renderSize.height);

rate = renderW / MIN(videoTrack.naturalSize.width, videoTrack.naturalSize.height);

//对视频的方向进行处理

CGAffineTransform translateToCenter;

CGAffineTransform mixedTransform;

NSInteger degrees = [self degressFromVideoFileWithURL:asset];

if (degrees == 0) {

if (videoTrack.naturalSize.width == videoTrack.naturalSize.height) {

translateToCenter = CGAffineTransformMakeTranslation(0.0, 0.0);

}else{

translateToCenter = CGAffineTransformMakeTranslation(-140.0, 0.0);

}

mixedTransform = CGAffineTransformRotate(translateToCenter,0);

}else{

if(degrees == 90){

//顺时针旋转90°

NSLog(@"视频旋转90度,home按键在左");

translateToCenter = CGAffineTransformMakeTranslation(videoTrack.naturalSize.height, -240);

mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI_2);

}else if(degrees == 180){

//顺时针旋转180°

NSLog(@"视频旋转180度,home按键在上");

translateToCenter = CGAffineTransformMakeTranslation(videoTrack.naturalSize.width, videoTrack.naturalSize.height);

mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI);

}else if(degrees == 270){

//顺时针旋转270°

NSLog(@"视频旋转270度,home按键在右");

translateToCenter = CGAffineTransformMakeTranslation(0.0, videoTrack.naturalSize.width);

mixedTransform = CGAffineTransformRotate(translateToCenter,M_PI_2*3.0);

}

}

//将处理好的视频赋值给AVMutableVideoCompositionLayerInstruction

[passThroughLayer setTransform:mixedTransform atTime:kCMTimeZero];

[passThroughLayer setOpacity:0.0 atTime:[asset duration]];

//然后将处理好的AVMutableVideoCompositionLayerInstruction赋值给AVMutableVideoCompositionInstruction

passThroughInstruction.timeRange = CMTimeRangeMake(kCMTimeZero, [asset duration]);

passThroughInstruction.layerInstructions = @[passThroughLayer];

//创建 video composition 

self.mutableVideoComposition.instructions = @[passThroughInstruction];

self.mutableVideoComposition.frameDuration = CMTimeMake(1, 30); // 30 fps

self.mutableVideoComposition.renderSize = CGSizeMake(renderW, renderW);

}

videoSize = self.mutableVideoComposition.renderSize;

//添加背景

self.watermarkLayer = [self watermarkLayerForSize:videoSize withImageNamed:imgName withColorName:color];

}

}

// Step 3 发送通知到你想要处理的界面

// Notify AVSEViewController about add watermark operation completion

[[NSNotificationCenter defaultCenter] postNotificationName:AVSEEditCommandCompletionNotification object:self];


// 这里执行添加背景边框 和 水印的 layer

- (CALayer*)watermarkLayerForSize:(CGSize)videoSize withImageNamed:(NSString*)imgName withColorName:(NSString *)color{

// Create a layer for the title

CALayer *titleLayer = [CALayer layer];

titleLayer.bounds = CGRectMake(0, 0, videoSize.width, videoSize.height);//此处的Frame为视频展示试图的大小

titleLayer.masksToBounds = true;

UIImage *image = [UIImage imageNamed:imgName];

titleLayer.contents = (id)image.CGImage;

titleLayer.position = CGPointMake(videoSize.width/2, videoSize.height/2);

//还能给背景设置样式和颜色

//do something...

return titleLayer;

}


- (void)exportWillBegin{

CALayer *parentLayer = [CALayer layer];

CALayer *videoLayer = [CALayer layer];

if (self.backGroundLayer) {

videoLayer.frame = CGRectMake(20, 20, self.videoComposition.renderSize.width - 40, self.videoComposition.renderSize.height - 40);

}else{

videoLayer.frame = CGRectMake(0, 0, self.videoComposition.renderSize.width, self.videoComposition.renderSize.height);

}

parentLayer.frame = CGRectMake(0, 0, self.videoComposition.renderSize.width, self.videoComposition.renderSize.height);

//这里是先添加背景backgroundLayer,然后再添加videoLayer,那么视频就会在背景上了。

//而添加水印则是相反的,我们把overlayLayer放在了videolayer的上面,所以水印总是显示在视频之上的。

[parentLayer addSublayer:backgroundLayer];

[parentLayer addSublayer:videoLayer];

//添加水印到视频组合器里

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

}

//之后导出就可以了

第二节:视频添加动画

添加动画的原理 和 添加水印是一样的 

- (CALayer*)watermarkLayerForSize:(CGSize)videoSize withImageNamed:(NSString*)imgName withColorName:(NSString *)color{

// Create a layer for the title

CALayer *overlayLayer1 = [CALayer layer];

[overlayLayer1 setContents:(id)[animationImage CGImage]];

overlayLayer1.frame = CGRectMake(size.width/2-64, size.height/2 + 200, 128, 128);

[overlayLayer1 setMasksToBounds:YES];

CABasicAnimation *animation = [CABasicAnimation animationWithKeyPath:@"transform.rotation"];

animation.duration=2.0;

animation.repeatCount=5;

animation.autoreverses=YES;

// rotate from 0 to 360

animation.fromValue=[NSNumber numberWithFloat:0.0];

animation.toValue=[NSNumber numberWithFloat:(2.0 * M_PI)];

animation.beginTime = AVCoreAnimationBeginTimeAtZero;       //注意一定要设置开始时间不然不显示

[overlayLayer1 addAnimation:animation forKey:@"rotation"];

return overlayLayer1;

}


第三节:视频添加音频

1.拿到视频和音频资源

2.创建AVMutableComposition对象

3.往AVMutableComposition对象添加视频资源,同时设置视频资源的时间段和插入点

4.往AVMutableComposition对象添加音频资源,同时设置音频资源的时间段和插入点

5.往AVMutableComposition对象添加要追加的音频资源,同时设置音频资源的时间段,插入点和混合模式

- (void)performWithAsset:(AVAsset*)asset withImageNamed:(NSString*)imgName withMusicName:(NSString *)musicName

{

AVAssetTrack *assetVideoTrack = nil;

AVAssetTrack *assetAudioTrack = nil;

// Check if the asset contains video and audio tracks

//    拿到视频和音频资源

if ([[asset tracksWithMediaType:AVMediaTypeVideo] count] != 0) {

assetVideoTrack = [asset tracksWithMediaType:AVMediaTypeVideo][0];

}

if ([[asset tracksWithMediaType:AVMediaTypeAudio] count] != 0) {

assetAudioTrack = [asset tracksWithMediaType:AVMediaTypeAudio][0];

}

NSError *error = nil;

// Step 1

NSArray *Arr = [NSArray array];

Arr = [musicName componentsSeparatedByString:@"."];

NSString *audioURL = [[NSBundle mainBundle] pathForResource:[Arr firstObject] ofType:[Arr lastObject]];

AVAsset *audioAsset = [[AVURLAsset alloc] initWithURL:[NSURL fileURLWithPath:audioURL] options:nil];

AVAssetTrack *newAudioTrack = [audioAsset tracksWithMediaType:AVMediaTypeAudio][0];

// Step 2

//    创建AVMutableComposition对象

if (!self.mutableComposition) {

// Check whether a composition has already been created, i.e, some other tool has already been applied.

// 创建  new composition组合器

self.mutableComposition = [AVMutableComposition composition];

// Add tracks to composition from the input video asset

if (assetVideoTrack != nil) {

//            往AVMutableComposition对象添加视频资源,同时设置视频资源的时间段和插入点

AVMutableCompositionTrack *compositionVideoTrack = [self.mutableComposition addMutableTrackWithMediaType:AVMediaTypeVideo preferredTrackID:kCMPersistentTrackID_Invalid];

[compositionVideoTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, [asset duration]) ofTrack:assetVideoTrack atTime:kCMTimeZero error:&error];

}

if (assetAudioTrack != nil) {

//          往AVMutableComposition对象添加音频资源, 同时设置音频资源的时间段和插入点

AVMutableCompositionTrack *compositionAudioTrack = [self.mutableComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];

[compositionAudioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, [asset duration]) ofTrack:assetAudioTrack atTime:kCMTimeZero error:&error];

}

}

// Step 3

//    添加音频资源到composition

AVMutableCompositionTrack *customAudioTrack = [self.mutableComposition addMutableTrackWithMediaType:AVMediaTypeAudio preferredTrackID:kCMPersistentTrackID_Invalid];

[customAudioTrack insertTimeRange:CMTimeRangeMake(kCMTimeZero, [self.mutableComposition duration]) ofTrack:newAudioTrack atTime:kCMTimeZero error:&error];

// Step 4

//    设置添加资源中的音频时间段,并与原有视频中的音频混合

AVMutableAudioMixInputParameters *mixParameters = [AVMutableAudioMixInputParameters audioMixInputParametersWithTrack:customAudioTrack];

[mixParameters setVolumeRampFromStartVolume:1 toEndVolume:0 timeRange:CMTimeRangeMake(kCMTimeZero, self.mutableComposition.duration)];

self.mutableAudioMix = [AVMutableAudioMix audioMix];

self.mutableAudioMix.inputParameters = @[mixParameters];

// Step 5 发送通知到你想要处理的界面,这里完成之后就可以直接导出了,因为音乐已经添加到对于的视频上了

[[NSNotificationCenter defaultCenter] postNotificationName:AVSEEditCommandCompletionNotification object:self];

}

需要注意的是:如果你的视频在界面上进行展示了,那在添加了音乐之后就要去刷新UI。我这里使用的是AVPlayer来进行播放

//视频播放试图

- (void)video:(NSString *)videoPath{

NSURL *url = [NSURL fileURLWithPath:videoPath];

self.player = [AVPlayer playerWithURL:url];

AVPlayerLayer *playerLayer = [AVPlayerLayer playerLayerWithPlayer:self.player];

playerLayer.bounds = self.photo.bounds;

playerLayer.position = CGPointMake(self.photo.bounds.size.width/2, self.photo.bounds.size.height/2);

playerLayer.videoGravity = AVLayerVideoGravityResizeAspectFill;//视频填充模式

[self.photo.layer addSublayer:playerLayer];

[self.player play];

[self reloadPlayerViewWithMusic];

}

- (void)playbackFinished:(NSNotification *)n{

//注册的通知  可以自动把 AVPlayerItem 对象传过来,只要接收一下就OK

AVPlayerItem * playerItem = [n object];

//关键代码

[playerItem seekToTime:kCMTimeZero];

[self.player play];

NSLog(@"重播");

}

// 添加音乐后 刷新视频播放器 

- (void)reloadPlayerViewWithMusic{

//    添加music

mutableAudioMix = self.audioMix;

AVPlayerItem * playerItem = [AVPlayerItem playerItemWithAsset:self.composition];

//    判断是否 有音频文件

if(self.videoComposition && self.audioMix){

playerItem.videoComposition = self.videoComposition;

playerItem.audioMix = self.audioMix;

[self.player replaceCurrentItemWithPlayerItem:playerItem];

}

}



第五节:视频导出

1.创建输出路径

2.根据AVMutableComposition对象创建AVAssetExportSession视频导出对象

3.设置AVAssetExportSession的AVMutableVideoComposition对象,AVMutableAudioMix对象,视频导出路径,视频导出格式

4.异步导出视频,根据导出结果做对应处理。

- (void)performWithAsset:(AVAsset*)asset withImageNamed:(NSString*)imgName withColorName:(NSString*)color withMusicName:(NSString *)musicName  with:(CGRect)photoSize{

// Step 1

//    创建输出路径

NSArray *paths = NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES);

NSString *documentsDirectory = [paths objectAtIndex:0];

NSString *outputURL =  [documentsDirectory stringByAppendingPathComponent:

[NSString stringWithFormat:@"FinalVideo-%d.m4v",arc4random() % 1000]];

// Step 2

//    创建导出对象

self.exportSession = [[AVAssetExportSession alloc] initWithAsset:self.mutableComposition presetName:AVAssetExportPresetHighestQuality];

//    视频组合器

self.exportSession.videoComposition = self.mutableVideoComposition;

//    音频组合器

self.exportSession.audioMix = self.mutableAudioMix;

self.exportSession.shouldOptimizeForNetworkUse = YES;

self.exportSession.outputURL = [NSURL fileURLWithPath:outputURL];

//    导出格式

self.exportSession.outputFileType = AVFileTypeQuickTimeMovie;

[self.exportSession exportAsynchronouslyWithCompletionHandler:^(void){

switch (self.exportSession.status) {

case AVAssetExportSessionStatusCompleted:{

dispatch_async(dispatch_get_main_queue(), ^{

//输出完成后

[self writeVideoToPhotoLibrary:self.exportSession.outputURL];

});

// Step 3  发送通知到指定的界面

[[NSNotificationCenter defaultCenter] postNotificationName:AVSEExportCommandCompletionNotification

object:self];

}

break;

case AVAssetExportSessionStatusFailed:

NSLog(@"Failed:%@",self.exportSession.error);

[[NSNotificationCenter defaultCenter] postNotificationName:@"ExportCommandFaild" object:nil];

break;

case AVAssetExportSessionStatusCancelled:

NSLog(@"Canceled:%@",self.exportSession.error);

break;

default:

break;

}

}];

}

- (void)writeVideoToPhotoLibrary:(NSURL *)url

{

ALAssetsLibrary *library = [[ALAssetsLibrary alloc] init];

//    保存视频到指定的相溥

[library saveVideo:url toAlbum:PHOTO_ALBUM_NAME completion:^(NSURL *assetURL, NSError *error) {

} failure:^(NSError *error) {

}];

}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容