GPUImage 学习三(水印效果)

作者: 古子林 | 来源:发表于2018-01-05 18:24 被阅读395次

在写之前必须要吐槽一下我此时此刻的心情:为了实现水印效果我在网上搜集了大量的资料,结果都是千篇一律的,除了极少数原著写的有营养外,其他 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,读取本地视频进行操作后再次保存没有声音的问题,网上一条资料都没找到,自己尝试了几种方法也都没有实现。有谁有解决方案或者思路的请留言提示。我也会在闲暇时间再来研究的。
2,本想再写一个加载 GIF 动图效果水印的,可公司看我最近太闲,又给安排新任务了,所以也以后再实现吧。

相关文章

网友评论

  • 牵绊Sunshine灬:非常感谢博主的贡献,我这用 GPUImageVideoCamera 调用相机录制加水印视频,但是测试人员用5c(10.2)的时候水印出不来,这个有遇到过这情况么?
    古子林:@牵绊Sunshine灬 闪烁是水印位置不停的抖动,还是水印时有时无的闪烁?
    牵绊Sunshine灬:@古子林 多谢,已排查到,因水印视图用xib搭建的,不知为何没能展示出来,改用纯代码就正常了。还有个问题请教下:在视频录制过程中,水印会闪烁,这是什么原因有遇到过么?
    古子林:1,可能是适配问题,导致水印的位置发生了偏移,2,此外手机10.2的版本问题的确比其他版本的多,这里也不确定是否跟系统bug有关。
    建议先排查1的情况,没有问题再用模拟器、真机对比其他系统版本是否有问题来排查是否为系统bug所致。
  • devzhaoyou:非常感谢博主的贡献,想问下,你添加文字水印的时候,有没有遇到文字水印模糊的情况呢?
    古子林:你的模糊应该是水印图层的bounds设置的比图片的bounds小,在合成的时候拉伸所导致的吧。

本文标题:GPUImage 学习三(水印效果)

本文链接:https://www.haomeiwen.com/subject/exssnxtx.html