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