在写之前必须要吐槽一下我此时此刻的心情:为了实现水印效果我在网上搜集了大量的资料,结果都是千篇一律的,除了极少数原著写的有营养外,其他 copy 代码的人我真是无力吐槽。原著中也很少有人把这一块所有的应用场景都讲解到,所以我就在此汇总一下。
本篇内容:
1,GPUImageMovie 的基本用法(包括实现有声音播放);
2,用 GPUImageVideoCamera 调用相机录制加水印视频;
3,用 GPUImageMovie 读取本地资源-->加水印-->用 GPUImageMovieWriter 保存到本地;
4,图片加水印
把这几种效果的公共代码部分放在最前面。
属性定义
@interface WatermarkViewController ()<GPUImageMovieDelegate>{
GPUImageMovie *_movie;
GPUImageMovieWriter *_movieWriter;
GPUImageVideoCamera *_videoCamera;
GPUImageView *_imageView; // 展示图像内容
UIView *_watermarkView; // 水印图层
GPUImageAlphaBlendFilter *_alphaBlendFilter; // 透明度混合滤镜,用来实现添加水印
NSString *_temPath; // 临时缓存路径
AVPlayer *_player; // 用来播放视频声音的
}
初始化部分
- (void)viewDidLoad {
[super viewDidLoad];
_imageView = [[GPUImageView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.width)];
_imageView.center = self.view.center;
[self.view addSubview:_imageView];
// 创建水印视图
_watermarkView = [[UIView alloc] initWithFrame:_imageView.bounds];
UILabel *label = [[UILabel alloc] initWithFrame:CGRectMake(0, 0, 80, 80)];
label.text = @"子林";
label.textColor = [UIColor whiteColor];
label.textAlignment = NSTextAlignmentCenter;
label.font = [UIFont systemFontOfSize:20 weight:UIFontWeightBold];
[_watermarkView addSubview:label];
// 创建透明度混合滤镜
_alphaBlendFilter = [[GPUImageAlphaBlendFilter alloc] init];
// 融合的比例,默认就是1.0
_alphaBlendFilter.mix = 1.0;
// 以下几种效果,想看哪种就调用哪个方法
// GPUImageMovie 的基本用法
// [self movieUsage];
// // 调用相机录制视频加水印
// [self cameraAddWartermark];
// // 读取本地资源-->加水印-->保存到本地
// [self readLocalResourceAddWatermark];
// // 图片加水印
[self pictureAddWatermark];
}
// 根据画面的宽高比,适配展示画面的视图
- (CGRect)frameWithAspectRatio:(CGFloat)ratio{
CGFloat w = self.view.frame.size.width;
CGFloat h = self.view.frame.size.width;
if (ratio > 1) {
h = w / ratio;
}
else{
w = h * ratio;
}
CGRect frame = CGRectMake(self.view.center.x - w / 2.0, self.view.center.y - h / 2.0, w, h);
return frame;
}
GPUImageMovie 的基本用法
GPUImageMovie 的功能是读取本地的视频文件。它继承于 GPUImageOutput,因此可以作为输出源把视频输出到 GPUImageView 对象上
GPUImageMovie方法的使用比较简单,实现代码如下:
- (void)movieUsage{
// GPUImageMovie 使用方法,GPUImageMovie 读取的视频显示在view上是没有声音的,需要添加AVPlayer对象播放声音
NSURL *movieUrl = [[NSBundle mainBundle] URLForResource:@"video_material_1" withExtension:@"mp4"];
// 获取视频的尺寸
AVAsset *fileas = [AVAsset assetWithURL:movieUrl];
CGSize movieSize = fileas.naturalSize;
// 适配视图的大小
_imageView.frame = [self frameWithAspectRatio:movieSize.width / movieSize.height];
_movie = [[GPUImageMovie alloc] initWithURL:movieUrl];
// 按视频真实帧率播放
_movie.playAtActualSpeed = YES;
// 重复播放
_movie.shouldRepeat = YES;
// 是否在控制台输出当前帧时间
_movie.runBenchmark = YES;
_movie.delegate = self;
[_movie addTarget:_imageView];
// 开始处理视频
[_movie startProcessing];
}
注意:
由于GPUImageView 继承自UIView,所以 GPUImageMovie 读取的视频显示在GPUImageView上是没有声音的,如果需要视频有声音,可以用 GPUImageMovie 的另一种方法进行初始化
- (id)initWithPlayerItem:(AVPlayerItem *)playerItem;
NSURL *movieUrl = [[NSBundle mainBundle] URLForResource:@"video_material_1" withExtension:@"mp4"];
AVAsset *fileas = [AVAsset assetWithURL:movieUrl];
CGSize movieSize = fileas.naturalSize;
_imageView.frame = [self frameWithAspectRatio:movieSize.width / movieSize.height];
AVPlayerItem *playerItem = [AVPlayerItem playerItemWithURL:movieUrl];
_player = [[AVPlayer alloc] initWithPlayerItem:playerItem];
_movie = [[GPUImageMovie alloc] initWithPlayerItem:playerItem];
_movie.playAtActualSpeed = YES;
_movie.shouldRepeat = YES;
_movie.runBenchmark = YES;
_movie.delegate = self;
[_movie addTarget:_imageView];
[_movie startProcessing];
// play 放在 startProcessing 之后
[_player play];
GPUImageMovie 声明了 GPUImageMovieDelegate 协议,用来处理视频播放完成的回调
// 监控 GPUImageMovie 播放完成状态,如果 shouldRepeat 设为 YES 则不会走这里
- (void)didCompletePlayingMovie{
NSLog(@"视频播放完成");
}
用 GPUImageVideoCamera 调用相机录制加水印视频
效果图:
相机水印效果.PNG
在代码之前先介绍两个类
- GPUImageUIElement
GPUImageUIElement,这个类可以近似理解为 GPUImagePicture,区别是它的数据来源不是UIImage,而是 UIView 或 CALayer,作为图像处理响应链的源头。
- GPUImageAlphaBlendFilter
从名称翻译就是透明度混合滤镜效果。我们的实现原理就是把水印视图 _watermarkView 和视频中的每一帧图片作为输入源经过 GPUImageAlphaBlendFilter 处理后生成新的图片作为播放的帧图片。
说明:有很多种混合效果可以用,在网上搜的很多是用的 GPUImageDissolveBlendFilter 处理的,用法跟 GPUImageAlphaBlendFilter一样,经自己测试发现,GPUImageDissolveBlendFilter 处理的水印会影响原视频的亮度,使视频变暗。
- (void)cameraAddWartermark{
_videoCamera = [[GPUImageVideoCamera alloc] initWithSessionPreset:AVCaptureSessionPreset640x480 cameraPosition:AVCaptureDevicePositionFront];
_videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait;
// 适配视图的大小
_imageView.frame = [self frameWithAspectRatio:480.0 / 640.0];
_watermarkView.frame = _imageView.bounds;
// 创建水印图形
GPUImageUIElement *uiElement = [[GPUImageUIElement alloc] initWithView:_watermarkView];
GPUImageFilter *videoFilter = [[GPUImageFilter alloc] init];
[_videoCamera addTarget:videoFilter];
[videoFilter addTarget:_alphaBlendFilter];
[uiElement addTarget:_alphaBlendFilter];
[_alphaBlendFilter addTarget:_imageView];
// GPUImageVideoCamera 开始捕获画面展示在 GPUImageView
[_videoCamera startCameraCapture];
__block GPUImageUIElement *weakElement = uiElement;
[videoFilter setFrameProcessingCompletionBlock:^(GPUImageOutput *output, CMTime time) {
[weakElement update];
}];
UIButton *recordBtn = [UIButton buttonWithType:UIButtonTypeCustom];
recordBtn.frame = CGRectMake(self.view.bounds.size.width / 2.0 - 30, self.view.bounds.size.height - 80, 60, 60);
[recordBtn setTitleColor:[UIColor orangeColor] forState:UIControlStateNormal];
[recordBtn setTitle:@"开始" forState:UIControlStateNormal];
[recordBtn setTitle:@"暂停" forState:UIControlStateSelected];
[recordBtn addTarget:self action:@selector(recordBtnAction:) forControlEvents:UIControlEventTouchUpInside];
[self.view addSubview:recordBtn];
}
- (void)recordBtnAction:(UIButton *)sender{
if (sender.selected) { // 录像状态
[_movieWriter finishRecording];
UISaveVideoAtPathToSavedPhotosAlbum(_temPath, nil, nil, nil);
[_alphaBlendFilter removeTarget:_movieWriter];
}
else{ // 没有录像
_temPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/Movie.mov"];
unlink([_temPath UTF8String]); // 判断路径是否存在,如果存在就删除路径下的文件,否则是没法缓存新的数据的。
NSURL *movieURL = [NSURL fileURLWithPath:_temPath];
_movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:movieURL size:CGSizeMake(640.0, 480.0)];
[_movieWriter setHasAudioTrack:YES audioSettings:nil];
_videoCamera.audioEncodingTarget = _movieWriter;
[_alphaBlendFilter addTarget:_movieWriter];
[_movieWriter startRecording];
}
sender.selected = !sender.selected;
}
注意:
1,需要注意的是数据链的添加顺序;
2,必须要为 _videoCamera 添加一个 filter 对象作为数据链的源头,不能直接用_alphaBlendFilter;
3,2 中的filter对象要通过 setFrameProcessingCompletionBlock 来实现界面的刷新(很多资料中说这样会导致内存不断增加,这个应该是他们代码写的有问题导致的,本人亲测没有内存不断增长的情况出现)
__block GPUImageUIElement *weakElement = uiElement;
[videoFilter setFrameProcessingCompletionBlock:^(GPUImageOutput *output, CMTime time) {
[weakElement update];
}];
GPUImageMovie 读取本地资源-->加水印-->用GPUImageMovieWriter 保存到本地
效果图:
本地视频加水印效果.png
- 这个需求耗费了有整整一天的时间了,网上只有少的可怜的一些资料,并且还都有问题,我下了其中两个的demo源码运行,根本就不行,太坑了,不知道他们为什么还要放到网上。在没有资料可以借鉴的情况下,我们只能一个一个坑的踩着往前挪了。
实现代码:
- (void)readLocalResourceAddWatermark {
NSURL *movieUrl = [[NSBundle mainBundle] URLForResource:@"video_material_2" withExtension:@"MOV"];
// 获取视频的尺寸
AVAsset *fileas = [AVAsset assetWithURL:movieUrl];
CGSize movieSize = fileas.naturalSize;
// 适配视图的大小
_imageView.frame = [self frameWithAspectRatio:movieSize.width / movieSize.height];
_watermarkView.frame = _imageView.bounds;
_movie = [[GPUImageMovie alloc] initWithURL:movieUrl];
_movie.playAtActualSpeed = YES;
_movie.shouldRepeat = NO;
_movie.runBenchmark = YES;
_movie.delegate = self;
// 创建水印图形
GPUImageUIElement *uiElement = [[GPUImageUIElement alloc] initWithView:_watermarkView];
GPUImageFilter *videoFilter = [[GPUImageFilter alloc] init];
[_movie addTarget:videoFilter];
[videoFilter addTarget:_alphaBlendFilter];
[uiElement addTarget:_alphaBlendFilter];
[_alphaBlendFilter addTarget:_imageView];
[_movie startProcessing];
__block GPUImageUIElement *weakElement = uiElement;
[videoFilter setFrameProcessingCompletionBlock:^(GPUImageOutput *output, CMTime time) {
[weakElement update];
}];
// GPUImageMovieWriter 视频编码
_temPath = [NSHomeDirectory() stringByAppendingPathComponent:@"Documents/Movie.m4v"];
unlink([_temPath UTF8String]); // 判断路径是否存在,如果存在就删除路径下的文件,否则是没法缓存新的数据的。
NSURL *movieWriterURL = [NSURL fileURLWithPath:_temPath];
_movieWriter = [[GPUImageMovieWriter alloc] initWithMovieURL:movieWriterURL size:movieSize];
_movieWriter.shouldPassthroughAudio = YES;
[_alphaBlendFilter addTarget:_movieWriter];
// 不要设置这两句,会导致内存不断升高
// _movieWriter.hasAudioTrack = NO;
// _movie.audioEncodingTarget = _movieWriter;
// 允许使用 GPUImageMovieWriter 进行音视频同步编码
[_movie enableSynchronizedEncodingUsingMovieWriter:_movieWriter];
[_movieWriter startRecording];
// 写入完成后可保存到相册
[_movieWriter setCompletionBlock:^{
NSLog(@"视频水印添加完成,可根据需要保存到本地或者进行其他操作");
NSLog(@"%@",NSHomeDirectory());
UISaveVideoAtPathToSavedPhotosAlbum(_temPath, nil, nil, nil);
}];
}
GPUImageMovieWriter 有个完成回调,我们可以在回调中进行后续操作(如保存,播放等)
[_movieWriter setCompletionBlock:^{
NSLog(@"视频水印添加完成,可根据需要保存到本地或者进行其他操作");
NSLog(@"%@",NSHomeDirectory());
UISaveVideoAtPathToSavedPhotosAlbum(_temPath, nil, nil, nil);
}];
注意:
1,_movieWriter 的 shouldPassthroughAudio 一定要设置,无论是设为 YES 还是 NO 都可以,这个属性为是否不处理视频中的音频,但设置 YES 和 NO 保存出来的视频都是没有声音的;
2,不要给设置以下两句,否则内存会不断增加,直至崩溃。个人理解是因为 _movie 输出到界面上的视频是没有声音的,所以会导致错误。但我用 AVPlayer 输出的初始化 _movie 也不行,我觉得应该是需要给 _movie.audioEncodingTarget赋值一个音轨对象应该可以。不过目前我还不知道该怎么做。
_movieWriter.hasAudioTrack = NO;
_movie.audioEncodingTarget = _movieWriter;
图片加水印
效果图:
图片水印效果.png
图片的实现就要简单很多了,几行代码一看就懂
- (void)pictureAddWatermark{
UIImage *image = [UIImage imageNamed:@"zilin.jpeg"];
// 适配视图的大小
_imageView.frame = [self frameWithAspectRatio:image.size.width / image.size.height];
_watermarkView.frame = _imageView.bounds;
GPUImagePicture *picture = [[GPUImagePicture alloc] initWithImage:image];
// 创建水印图形
GPUImageUIElement *uiElement = [[GPUImageUIElement alloc] initWithView:_watermarkView];
GPUImageFilter *imageFilter = [[GPUImageFilter alloc] init];
[picture addTarget:imageFilter];
[imageFilter addTarget:_alphaBlendFilter];
[uiElement addTarget:_alphaBlendFilter];
[_alphaBlendFilter addTarget:_imageView];
[picture processImage];
__block GPUImageUIElement *weakElement = uiElement;
[imageFilter setFrameProcessingCompletionBlock:^(GPUImageOutput *output, CMTime time) {
[weakElement update];
}];
}
网友评论
建议先排查1的情况,没有问题再用模拟器、真机对比其他系统版本是否有问题来排查是否为系统bug所致。