当根据https://blog.csdn.net/weixin_42433480/article/details/90112917录制完视频并根据https://blog.csdn.net/weixin_42433480/article/details/90109873将断点的视频结合在一起导出后,就要开始编辑视频。
整个编辑过程分为五大部分,下面我们一一列举出来。
(一)预览视频:
这里使用的是GPUImageMovie,这个类仅仅支持本地视频文件播放,不支持在线视频播放。
核心代码:
movieFile = [[GPUImageMovie alloc] initWithPlayerItem:playerItem];
//这使当前视频处于基准测试的模式,记录并输出瞬时和平均帧时间到控制台 每隔一段时间打印: Current frame time : 51.256001 ms,直到播放或加滤镜等操作完毕
movieFile.runBenchmark = YES;
//控制GPUImageView预览视频时的速度是否要保持真实的速度。
//如果设为NO,则会将视频的所有帧无间隔渲染,导致速度非常快。
//设为YES,则会根据视频本身时长计算出每帧的时间间隔,然后每渲染一帧,就sleep一个时间间隔,从而达到正常的播放速度。
movieFile.playAtActualSpeed = YES;
filter = [[LFGPUImageEmptyFilter alloc] init];
_filtClassName = @"LFGPUImageEmptyFilter";
[movieFile addTarget:filter];
_filterView = [[GPUImageView alloc] initWithFrame:self.view.bounds];
[self.view addSubview:_filterView];
[filter addTarget:_filterView];
由于之前filter和GPUImageView在前两篇文章已经详细介绍,这里就不赘述了。关于GPUIMageMovie我们需要注意两点,一是当我们要把runBenchmark这个属性设为YES,这样我们可以使当前视频处于基准测试的模式,记录并输出瞬时和平均帧时间到控制台每隔一段时间打印: Current frame time : 51.256001 ms,直到播放或加滤镜等操作完毕。二是playAtActualSpeed属性需要设置为YES,这个属性是用于控制GPUImageView预览视频时的速度是否要保持真实的速度,如果设为NO,则会将视频的所有帧无间隔渲染,导致速度非常快,设为YES,则会根据视频本身时长计算出每帧的时间间隔,然后每渲染一帧,就sleep一个时间间隔,从而达到正常的播放速度。
(二)多滤镜的添加
首先我们要创建多个滤镜(用可滑动的UICollectionView展示):
-(NSArray*)creatFilterData
{
FilterData* filter1 = [self createWithName:@"Empty" andFlieName:@"LFGPUImageEmptyFilter" andValue:nil];
filter1.isSelected = YES;
FilterData* filter2 = [self createWithName:@"Amatorka" andFlieName:@"GPUImageAmatorkaFilter" andValue:nil];
FilterData* filter3 = [self createWithName:@"MissEtikate" andFlieName:@"GPUImageMissEtikateFilter" andValue:nil];
FilterData* filter4 = [self createWithName:@"Sepia" andFlieName:@"GPUImageSepiaFilter" andValue:nil];
FilterData* filter5 = [self createWithName:@"Sketch" andFlieName:@"GPUImageSketchFilter" andValue:nil];
FilterData* filter6 = [self createWithName:@"SoftElegance" andFlieName:@"GPUImageSoftEleganceFilter" andValue:nil];
FilterData* filter7 = [self createWithName:@"Toon" andFlieName:@"GPUImageToonFilter" andValue:nil];
FilterData* filter8 = [[FilterData alloc] init];
filter8.name = @"Saturation0";
filter8.iconPath = [[NSBundle mainBundle] pathForResource:@"GPUImageSaturationFilter0" ofType:@"png"];
filter8.fillterName = @"GPUImageSaturationFilter";
filter8.value = @"0";
FilterData* filter9 = [[FilterData alloc] init];
filter9.name = @"Saturation2";
filter9.iconPath = [[NSBundle mainBundle] pathForResource:@"GPUImageSaturationFilter2" ofType:@"png"];
filter9.fillterName = @"GPUImageSaturationFilter";
filter9.value = @"2";
return [NSArray arrayWithObjects:filter1,filter2,filter3,filter4,filter5,filter6,filter7,filter8,filter9, nil];
}
点击代表每一个滤镜的UICollectionViewCell来添加滤镜显示的代码:
if (_lastFilterIndex.row != _nowFilterIndex.row) {
//1.修改数据源
FilterData* dataNow = [_filterAry objectAtIndex:indexPath.row];
dataNow.isSelected = YES;
[_filterAry replaceObjectAtIndex:indexPath.row withObject:dataNow];
FilterData* dataLast = [_filterAry objectAtIndex:_lastFilterIndex.row];
dataLast.isSelected = NO;
[_filterAry replaceObjectAtIndex:_lastFilterIndex.row withObject:dataLast];
//2.刷新collectionView
[_collectionView reloadData];
_lastFilterIndex = indexPath;
}
if (indexPath.row == 0) {
[movieFile removeAllTargets];
FilterData* data = [_filterAry objectAtIndex:indexPath.row];
_filtClassName = data.fillterName;
filter = [[NSClassFromString(_filtClassName) alloc] init];
[movieFile addTarget:filter];
[filter addTarget:_filterView];
}else
{
[movieFile removeAllTargets];
FilterData* data = [_filterAry objectAtIndex:indexPath.row];
_filtClassName = data.fillterName;
if ([data.fillterName isEqualToString:@"GPUImageSaturationFilter"]) {
GPUImageSaturationFilter* xxxxfilter = [[NSClassFromString(_filtClassName) alloc] init];
xxxxfilter.saturation = [data.value floatValue];
_saturationValue = [data.value floatValue];
filter = xxxxfilter;
}else{
filter = [[NSClassFromString(_filtClassName) alloc] init];
}
[movieFile addTarget:filter];
// Only rotate the video for display, leave orientation the same for recording
[filter addTarget:_filterView];
}
这的实现原理其实是移除原有的滤镜,根据你点击的UICollectionViewCell所代表的滤镜给GPUImageMovie添加新的滤镜。
(三)背景音乐的添加
首先我们要先获得背景音乐的数据源:
-(NSArray*)creatMusicData
{
NSString *configPath = [[NSBundle mainBundle] pathForResource:@"music2" ofType:@"json"];
NSData *configData = [NSData dataWithContentsOfFile:configPath];
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:configData options:NSJSONReadingAllowFragments error:nil];
NSArray *items = dic[@"music"];
int i = 529 ;
NSMutableArray *array = [NSMutableArray array];
MusicData *effect = [[MusicData alloc] init];
effect.name = @"原始";
effect.iconPath = [[NSBundle mainBundle] pathForResource:@"nilMusic" ofType:@"png"];
effect.isSelected = YES;
[array addObject:effect];
for (NSDictionary *item in items) {
// NSString *path = [baseDir stringByAppendingPathComponent:item[@"resourceUrl"]];
MusicData *effect = [[MusicData alloc] init];
effect.name = item[@"name"];
effect.eid = item[@"id"];
effect.musicPath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"audio%d",i] ofType:@"mp3"];
effect.iconPath = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"icon%d",i] ofType:@"png"];
[array addObject:effect];
i++;
}
return array;
}
这里的数据源主要是本地模拟JSON数据(music2.json):
{
"music": [{
"resourceUrl": "music/No Limit",
"id": 1,
"name": "No Limit"
}, {
"resourceUrl": "music/皮皮虾我们走",
"id": 2,
"name": "皮皮虾我们走"
}, {
"resourceUrl": "music/Dragostea Din Teï",
"id": 3,
"name": "Dragostea Din Teï"
}, {
"resourceUrl": "music/还不是因为你长得不好看",
"id": 4,
"name": "还不是因为你长得不好看"
}, {
"resourceUrl": "music/Grass Studio",
"id": 5,
"name": "Grass Studio"
}, {
"resourceUrl": "music/Secret",
"id": 6,
"name": "一人饮酒醉"
}, {
"resourceUrl": "music/WHISTLE",
"id": 7,
"name": "WHISTLE"
}, {
"resourceUrl": "music/稻香",
"id": 8,
"name": "稻香"
}, {
"resourceUrl": "music/北京小妞",
"id": 9,
"name": "北京小妞"
}, {
"resourceUrl": "music/刀剑如梦",
"id": 10,
"name": "刀剑如梦"
}, {
"resourceUrl": "music/Glad You Came",
"id": 11,
"name": "Glad You Came"
}, {
"resourceUrl": "music/减肥",
"id": 12,
"name": "减肥"
}, {
"resourceUrl": "music/We Don't Talk Anymore",
"id": 13,
"name": "We Don't Talk Anymore"
}, {
"resourceUrl": "music/叫我女王",
"id": 14,
"name": "叫我女王"
}, {
"resourceUrl": "music/可惜不是你",
"id": 15,
"name": "可惜不是你"
}, {
"resourceUrl": "music/岁月神偷",
"id": 16,
"name": "岁月神偷"
}, {
"resourceUrl": "music/我好想你",
"id": 17,
"name": "我好想你"
}, {
"resourceUrl": "music/Show Me Your Bba Sae",
"id": 18,
"name": "Show Me Your Bba Sae"
}, {
"resourceUrl": "music/Candy★Night",
"id": 19,
"name": "Candy★Night"
}, {
"resourceUrl": "music/Again",
"id": 20,
"name": "Again"
}],
}
点击代表每一个背景音乐的UICollectionViewCell来播放背景音乐的代码:
_nowMusicIndex = indexPath;
if (_lastMusicIndex.row != _nowMusicIndex.row) {
//1.修改数据源
FilterData* dataNow = [_musicAry objectAtIndex:indexPath.row];
dataNow.isSelected = YES;
[_musicAry replaceObjectAtIndex:indexPath.row withObject:dataNow];
FilterData* dataLast = [_musicAry objectAtIndex:_lastMusicIndex.row];
dataLast.isSelected = NO;
[_musicAry replaceObjectAtIndex:_lastMusicIndex.row withObject:dataLast];
//刷新collectionView
[_musicCollectionView reloadData];
_lastMusicIndex = indexPath;
}
if (indexPath.row == 0) {
_audioPath = nil;
[_audioPlayer pause];
_editTheOriginaBtn.hidden = YES;
_editTheOriginaSwitch.hidden = YES;
[mainPlayer setVolume:1];
_editTheOriginaBtn.selected = NO;
_editTheOriginaSwitch.on = NO;
}else
{
MusicData* data = [_musicAry objectAtIndex:indexPath.row];
_audioPath = data.musicPath;
[self playMusic];
_editTheOriginaBtn.hidden = NO;
_editTheOriginaSwitch.hidden = NO;
}
}
-(void)playMusic
{
// 路径
NSURL *audioInputUrl = [NSURL fileURLWithPath:_audioPath];
// 声音来源
audioPlayerItem =[AVPlayerItem playerItemWithURL:audioInputUrl];
[_audioPlayer replaceCurrentItemWithPlayerItem:audioPlayerItem];//_audioPlayer = [[AVPlayer alloc ]init];
[_audioPlayer play];
}
这里还加了一个比较特别的可以剔除原声的功能:
//mainPlayer = [[AVPlayer alloc] init];playerItem = [[AVPlayerItem alloc] initWithURL:videoURL];[mainPlayer replaceCurrentItemWithPlayerItem:playerItem];
if (!_editTheOriginaBtn.selected) {
_editTheOriginaBtn.selected = YES;
[mainPlayer setVolume:0];//剔除原声
}else
{
[mainPlayer setVolume:1];//恢复原声
_editTheOriginaBtn.selected = NO;
}
注意:[mainPlayer setVolume:0]用于剔除原声,而[mainPlayer setVolume:1]用于恢复原声。
(四)添加贴纸
和背景音乐一样,也是用本地的JSON文件(stickers.json)模仿数据源:
-(NSArray*)creatStickersData
{
NSString *configPath = [[NSBundle mainBundle] pathForResource:@"stickers" ofType:@"json"];
NSData *configData = [NSData dataWithContentsOfFile:configPath];
NSDictionary *dic = [NSJSONSerialization JSONObjectWithData:configData options:NSJSONReadingAllowFragments error:nil];
NSArray *items = dic[@"stickers"];
int i = 529 ;
NSMutableArray *array = [NSMutableArray array];
for (NSDictionary *item in items) {
// NSString *path = [baseDir stringByAppendingPathComponent:item[@"resourceUrl"]];
StickersData* stickersItem = [[StickersData alloc] init];
stickersItem.name = item[@"name"];
stickersItem.StickersImgPaht = [[NSBundle mainBundle] pathForResource:[NSString stringWithFormat:@"stickers%d",i] ofType:@"jpg"];
[array addObject:stickersItem];
i++;
}
return array;
}
stickers.json文件:
{
"stickers": [
{
"resourceUrl": "music/Athena",
"name": "胸口碎大石"
},
{
"resourceUrl": "music/Athena",
"name": "我不胖"
},
{
"resourceUrl": "music/Athena",
"name": "完美"
},
{
"resourceUrl": "music/Athena",
"name": "我的脑残"
},
{
"resourceUrl": "music/Athena",
"name": "变美光波"
},
{
"resourceUrl": "music/Athena",
"name": "点个赞"
},
{
"resourceUrl": "music/Athena",
"name": "冷漠"
},
{
"resourceUrl": "music/Athena",
"name": "么么哒"
},
{
"resourceUrl": "music/Athena",
"name": "怕不怕"
},
{
"resourceUrl": "music/Athena",
"name": "出去浪"
},
{
"resourceUrl": "music/Athena",
"name": "给你小鱼干"
},
{
"resourceUrl": "music/Athena",
"name": "小祖宗"
},
{
"resourceUrl": "music/Athena",
"name": "好耀眼"
},
{
"resourceUrl": "music/Athena",
"name": "约吗"
},
{
"resourceUrl": "music/Athena",
"name": "打飞机"
},
{
"resourceUrl": "music/Athena",
"name": "辣眼睛"
},
{
"resourceUrl": "music/Athena",
"name": "喀嗞~喀嗞"
},
{
"resourceUrl": "music/Athena",
"name": "不给"
},
{
"resourceUrl": "music/Athena",
"name": "包裹到了"
},
{
"resourceUrl": "music/Athena",
"name": "哎呦~"
},
{
"resourceUrl": "music/Athena",
"name": "关注我哦"
},
{
"resourceUrl": "music/Athena",
"name": "我美吗"
},
{
"resourceUrl": "music/Athena",
"name": "帅成狗"
},
{
"resourceUrl": "music/Athena",
"name": "女王大人"
},
{
"resourceUrl": "music/Athena",
"name": "厉害了我的哥"
},
{
"resourceUrl": "music/Athena",
"name": "原地爆炸"
},
{
"resourceUrl": "music/Athena",
"name": "我经历了什么"
},
{
"resourceUrl": "music/Athena",
"name": "小淑女"
},
{
"resourceUrl": "music/Athena",
"name": "美少女"
},
{
"resourceUrl": "music/Athena",
"name": "你丑你先睡"
},
{
"resourceUrl": "music/Athena",
"name": "没穿秋裤"
},
{
"resourceUrl": "music/Athena",
"name": "颜值爆表耶"
},
{
"resourceUrl": "music/Athena",
"name": "吻我"
},
{
"resourceUrl": "music/Athena",
"name": "老鲜肉"
},
{
"resourceUrl": "music/Athena",
"name": "高冷"
},
{
"resourceUrl": "music/Athena",
"name": "要抱抱"
}
],
}
点击代表每一个贴纸的UICollectionViewCell来添加贴纸的代码:
_nowStickersIndex = indexPath;
if (_lastStickersIndex.row != _nowStickersIndex.row) {
//1.修改数据源
// FilterData* dataNow = [_filterAry objectAtIndex:indexPath.row];
StickersData* dataNow = [_stickersAry objectAtIndex:indexPath.row];
dataNow.isSelected = YES;
[_stickersAry replaceObjectAtIndex:indexPath.row withObject:dataNow];
StickersData* dataLast = [_stickersAry objectAtIndex:_lastStickersIndex.row];
dataLast.isSelected = NO;
[_stickersAry replaceObjectAtIndex:_lastStickersIndex.row withObject:dataLast];
//2.刷新collectionView
[_stickersCollectionView reloadData];
_lastStickersIndex = indexPath;
}else
{
_stickersImgView.center = CGPointMake(SCREEN_WIDTH/2, SCREEN_HEIGHT/2);
}
StickersData* data = [_stickersAry objectAtIndex:indexPath.row];
_stickersImgView.image = [UIImage imageWithContentsOfFile:data.StickersImgPaht];
_stickersImgView.hidden = NO;
这里实现的原理是每当点击代表贴纸的UICollectionViewCell时,就会将贴纸对象存储的image路径转化为图片并赋值给之前隐藏的UIImageView,这样我们就可以看到贴纸图片,但是这还不够,想要拖动贴纸,就需要添加手势代码了:
- (void) panView:(UIPanGestureRecognizer *)panGestureRecognizer
{
UIView *view = panGestureRecognizer.view;
if (panGestureRecognizer.state == UIGestureRecognizerStateBegan) {
_musicBottomBar.hidden = YES;//_musicBottomBar是包含滤镜,音乐,贴纸三个UICollectionView的View
}
if ( panGestureRecognizer.state == UIGestureRecognizerStateChanged) {
CGPoint translation = [panGestureRecognizer translationInView:view.superview];
[view setCenter:(CGPoint){view.center.x + translation.x, view.center.y + translation.y}];
[panGestureRecognizer setTranslation:CGPointZero inView:view.superview];
}
if (panGestureRecognizer.state == UIGestureRecognizerStateEnded) {
_musicBottomBar.hidden = NO;//_musicBottomBar是包含滤镜,音乐,贴纸三个UICollectionView的View
}
}
(五)音视频合成与压缩
由于音视频合成在前两篇文章已经详细叙述过了,那么接下来我们只说一下压缩视频。毋庸置疑,当我们拍摄完视频要上传时就必须要压缩视频,由于系统的压缩方法达不到质量大小与清晰度相匹配的需求,所以使用SDAVAssetExportSession
/* Create Output File Url */
NSString *documentsDirectory = NSTemporaryDirectory();
NSDateFormatter *formatter = [[NSDateFormatter alloc] init];
formatter.dateFormat = @"yyyyMMddHHmmss";
NSString *nowTimeStr = [formatter stringFromDate:[NSDate dateWithTimeIntervalSinceNow:0]];
NSString *finalVideoURLString = [documentsDirectory stringByAppendingPathComponent:[NSString stringWithFormat:@"compressedVideo%@.mp4",nowTimeStr]];
NSURL *outputVideoUrl = ([[NSURL URLWithString:finalVideoURLString] isFileURL] == 1)?([NSURL URLWithString:finalVideoURLString]):([NSURL fileURLWithPath:finalVideoURLString]); // Url Should be a file Url, so here we check and convert it into a file Url
NSDictionary* options = @{AVURLAssetPreferPreciseDurationAndTimingKey:@YES};
AVAsset* asset = [AVURLAsset URLAssetWithURL:inputVideoUrl options:options];
NSArray* keys = @[@"tracks",@"duration",@"commonMetadata"];
//loadValuesAsynchronouslyForKeys: https://blog.csdn.net/hdfqq188816190/article/details/72930381
[asset loadValuesAsynchronouslyForKeys:keys completionHandler:^{
//系统的压缩方法达不到质量大小与清晰度相匹配的需求,所以使用SDAVAssetExportSession:视频压缩https://www.jianshu.com/p/873ac13b6ce3
SDAVAssetExportSession *compressionEncoder = [SDAVAssetExportSession.alloc initWithAsset:asset]; // provide inputVideo Url Here
compressionEncoder.outputFileType = AVFileTypeMPEG4;
compressionEncoder.outputURL = outputVideoUrl; //Provide output video Url here
compressionEncoder.videoSettings = @
{
AVVideoCodecKey: AVVideoCodecH264,
AVVideoWidthKey: @720, //Set your resolution width here
AVVideoHeightKey: @1280, //set your resolution height here
AVVideoCompressionPropertiesKey: @
{
//2000*1000 建议800*1000-5000*1000
//AVVideoAverageBitRateKey: @2500000, // Give your bitrate here for lower size give low values
AVVideoAverageBitRateKey: _bit,
AVVideoProfileLevelKey: AVVideoProfileLevelH264HighAutoLevel,
AVVideoAverageNonDroppableFrameRateKey: _frameRate,
},
};
compressionEncoder.audioSettings = @
{
AVFormatIDKey: @(kAudioFormatMPEG4AAC),
AVNumberOfChannelsKey: @2,
AVSampleRateKey: @44100,
AVEncoderBitRateKey: @128000,
};
[compressionEncoder exportAsynchronouslyWithCompletionHandler:^
{
dispatch_async(dispatch_get_main_queue(), ^{
//更新UI操作
//.....
if (compressionEncoder.status == AVAssetExportSessionStatusCompleted)
{
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
HUD.hidden = YES;
[[NSNotificationCenter defaultCenter] removeObserver:self];
EditingPublishingDynamicViewController* cor = [[EditingPublishingDynamicViewController alloc] init];
cor.videoURL = compressionEncoder.outputURL;
// [[AppDelegate appDelegate] pushViewController:cor animated:YES];
[self.rt_navigationController pushViewController:cor animated:YES complete:nil];
});
}
else if (compressionEncoder.status == AVAssetExportSessionStatusCancelled)
{
// HUD.labelText = @"Compression Failed";
HUD.label.text = @"Compression Failed";
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] removeObserver:self];
// [[NSNotificationCenter defaultCenter] postNotificationName:kTabBarHiddenNONotification object:self];
[self.navigationController popToRootViewControllerAnimated:YES];
});
}
else
{
HUD.label.text = @"ompression Failed";
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(1.5 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
[[NSNotificationCenter defaultCenter] removeObserver:self];
// [[NSNotificationCenter defaultCenter] postNotificationName:kTabBarHiddenNONotification object:self];
[self.navigationController popToRootViewControllerAnimated:YES];
});
}
});
}];
}];
注意:这里的实现原理是我们先要根据url获取到AVAsset,然后将AVAsset进行压缩,这里我们会首先走loadValuesAsynchronouslyForKeys这个方法来判断你想要的值是否获取成功(https://blog.csdn.net/hdfqq188816190/article/details/72930381这里有较为详细的解释)。
然后通过SDAVAssetExportSession进行压缩,这里要设置outputFileType即压缩文件类型为AVFileTypeMPEG4,videoSettings包含视频格式为H264,分辨率为(720, 1280),还有传递给视频编码器的属性,比如AVVideoAverageBitRateKey(视频尺寸*比率)、AVVideoProfileLevelKey(画质,这里设为高画质) 、AVVideoAverageNonDroppableFrameRateKey(设置每秒不可掉落的视频的帧数), 这里我们要说下为何会有不可掉落帧:因为一些视频编码器可以产生一个灵活的混合非下降帧和下降帧。这两种类型的区别在于,为了成功解码后续帧,视频解码器需要解码一个不可下降帧,而可下降帧是可选的,可以跳过,不影响后续帧的解码。在一个序列中拥有一定比例的可丢弃帧对于时间可伸缩性具有优势:在回放时,可以根据播放速率解码更多或更少的帧。此属性要求编码器发出不可下降帧和可下降帧的总体比例,以便每秒有指定数量的不可下降帧。
此外还要对音频进行设置,比如audioSettings包含AVFormatIDKey(音频格式为aac)、AVNumberOfChannelsKey(通道数,这里设为2)、AVSampleRateKey(采样率,这里44100)、AVEncoderBitRateKey(比特采样率, 这里设为128000),关于SDAVAssetExportSession更详细的使用,https://www.jianshu.com/p/873ac13b6ce3这里有较为详细的解释,当这一切做完,我们再在主线程更新UI和跳转下一个发布界面就大功告成了。