笔记-GPUImage(三)短视频录制实时滤镜以及滤镜的切换

短视频实时滤镜(GPUImageVideoCamera)

demo下载地址:https://github.com/SXDgit/ZB_GPUImageVideoCamera

先看效果图:

image

直接上代码,后面解释:

- (void)createVideoCamera {
    // 创建画面捕获器
    self.videoCamera = [[GPUImageVideoCamera alloc]initWithSessionPreset:AVCaptureSessionPreset640x480 cameraPosition:AVCaptureDevicePositionBack];
    // 输出方向为竖屏
    self.videoCamera.outputImageOrientation = UIInterfaceOrientationPortrait;
    self.videoCamera.horizontallyMirrorRearFacingCamera = NO;
    self.videoCamera.horizontallyMirrorFrontFacingCamera = NO;
    self.videoCamera.runBenchmark = YES;
    
    // 构建组合滤镜
    [self addGPUImageFilter:self.sepiaFilter];
    [self addGPUImageFilter:self.monochromeFilter];
    
    // 创建画面呈现控件
    self.filterView = [[GPUImageView alloc]initWithFrame:self.view.frame];
    self.filterView.fillMode = kGPUImageFillModePreserveAspectRatio;
    self.view = self.filterView;
    
    [self.videoCamera addTarget:self.filterView];
    // 相机运行
    [self.videoCamera startCameraCapture];
    [self configMovie];
}

- (void)configMovie {
    // 设置写入地址
    self.pathToMovie = [NSHomeDirectory() stringByAppendingPathComponent:[NSString stringWithFormat:@"Documents/ZBMovied%u.mp4", arc4random() % 1000]];
    // movieUrl指视频写入的地址
    self.moviewURL = [NSURL fileURLWithPath:_pathToMovie];
    self.movieWriter = [[GPUImageMovieWriter alloc]initWithMovieURL:_moviewURL size:CGSizeMake(480.0, 640.0)];
    _movieWriter.encodingLiveVideo = YES;
    // 设置声音
    _videoCamera.audioEncodingTarget = _movieWriter;
}

- (void)addGPUImageFilter:(GPUImageFilter *)filter {
    [self.filterGroup addFilter:filter];
    
    GPUImageOutput<GPUImageInput> *newTerminalFilter = filter;
    NSInteger count = self.filterGroup.filterCount;
    if (count == 1) {
        self.filterGroup.initialFilters = @[newTerminalFilter];
        self.filterGroup.terminalFilter = newTerminalFilter;
    }else {
        GPUImageOutput<GPUImageInput> *terminalFilter = self.filterGroup.terminalFilter;
        NSArray *initialFilters = self.filterGroup.initialFilters;
        [terminalFilter addTarget:newTerminalFilter];
        self.filterGroup.initialFilters = @[initialFilters[0]];
        self.filterGroup.terminalFilter = newTerminalFilter;
    }
}

- (void)switchButtonAction {
    // 切换摄像头前后翻转
    [self.videoCamera rotateCamera];
    self.switchButton.selected = !self.switchButton.selected;
}

GPUImageFilter是用来接收源图像,通过自定义的顶点、片元着色器来渲染新的图像,并在绘制完成后通知响应链的下一个对象。
GPUImageVideoCamera提供来自摄像头的图像数据作为源数据,是GPUImageOutput的子类,一般是响应链的源头。
GPUImageView一般用于显示GPUImage的图像,是响应链的终点。
GPUImageFilterGroup是多个filter的集合,terminalFilter为最终的filter,initialFilter是filter数组。本身不绘制图像,对它的添加删除Target操作,都会转为terminalFilter的操作。

image

GPUImageMovieWriter是和GPUImageView处于同一地位的,都是视频输出类,只不过一个是输出到文件,一个输出到屏幕。
GPUImageBeautifyFilter基于GPUImage的实时美颜滤镜中的美颜滤镜,来自琨君。它是继承于GPUImageFilterGroup。包括了GPUImageBilateralFilterGPUImageCannyEdgeDetectionFilterGPUImageCombinationFilterGPUImageHSBFilter

绘制流程

来自GPUImage详细解析(三)- 实时美颜滤镜

image

  • 1、GPUImageVideoCamera捕获摄像头图像调用newFrameReadyAtTime: atIndex:通知GPUImageBeautifyFilter
  • 2、GPUImageBeautifyFilter调用newFrameReadyAtTime: atIndex:通知GPUImageBilateralFliter输入纹理已经准备好;
  • 3、GPUImageBilateralFliter 绘制图像后在informTargetsAboutNewFrameAtTime(),调用setInputFramebufferForTarget: atIndex:把绘制的图像设置为GPUImageCombinationFilter输入纹理,并通知GPUImageCombinationFilter纹理已经绘制完毕;
  • 4、GPUImageBeautifyFilter调用newFrameReadyAtTime: atIndex:通知 GPUImageCannyEdgeDetectionFilter输入纹理已经准备好;
  • 5、同3,GPUImageCannyEdgeDetectionFilter 绘制图像后,把图像设置为GPUImageCombinationFilter输入纹理;
  • 6、GPUImageBeautifyFilter调用newFrameReadyAtTime: atIndex:通知 GPUImageCombinationFilter输入纹理已经准备好;
  • 7、GPUImageCombinationFilter判断是否有三个纹理,三个纹理都已经准备好后调用GPUImageThreeInputFilter的绘制函数renderToTextureWithVertices: textureCoordinates:,图像绘制完后,把图像设置为GPUImageHSBFilter的输入纹理,通知GPUImageHSBFilter纹理已经绘制完毕;
  • 8、GPUImageHSBFilter调用renderToTextureWithVertices: textureCoordinates:绘制图像,完成后把图像设置为GPUImageView的输入纹理,并通知GPUImageView输入纹理已经绘制完毕;
  • 9、GPUImageView把输入纹理绘制到自己的帧缓存,然后通过[self.context presentRenderbuffer:GL_RENDERBUFFER]显示到UIView上。

核心思路

通过GPUImageVideoCamera采集音视频的信息,音频信息直接发送给GPUImageMovieWriter,视频信息传入响应链作为源头,渲染后的视频信息再写入GPUImageMovieWriter,同时GPUImageView显示再屏幕上。

image

通过源码可以知道GPUImage是使用AVFoundation框架来获取视频的。
AVCaptureSession类从AV输入设备的采集数据到制定的输出。
为了实现实时的图像捕获,要实现AVCaptureSession类,添加合适的输入(AVCaptureDeviceInput)和输出(比如AVCaptureMovieFileOutput)调用startRunning开始输入到输出的数据流,调用stopRunning停止数据流。需要注意的是startingRunning函数会花费一定的时间,所以不能在主线程调用,防止卡顿。

image

流程解析:
1、找到物理设备摄像头_inputCamera、麦克风_microphone,创建摄像头输入videoInput和麦克风输入audioInput
2、设置videoInputaudioInput_captureSession的输入,同时设置videoOutputaudioOutput_captureSession的输出,并且设置videoOutputaudioOutput的输出delegate
3、_captureSession调用startRunning,开始捕获信号。
4、音频数据到达,把数据转发给之前设置的audioEncodingTarget,并通过调用assetWriterAudioInputappendSampleBuffer方法写入音频数据。
5、视频数据到达,视频数据传入响应链,经过处理后通过assetWriterPixelBufferInputappendSampleBuffer方法写入视频数据。
6、视频录制完成,保存写入手机相册。

踩过的坑

1、录制后保存在相册里的视频是白屏?
在初始化movieWriter的过程中,使用addTarget:增加了滤镜导致。

2、录制完视频后,再次点击录制,会crash?
报错的原因是[AVAssetWriter startWriting] Cannot call method when status is 3,报错是在[self.movieWriter startRecording];这行代码,追溯源码,可以看到GPUImageMovieWriter是对AssetWriter进行了一次封装,其核心的写文件还是由AssetWriter完成。
通过源码可以发现[self.movieWriter finishRecording];以后并没有新建一个AssetWriter实例。所以可以保存视频到相册成功后,加入下面几行代码:

- (void)videoCameraReset {
    [_videoCamera removeTarget:_movieWriter];
    [[NSFileManager defaultManager] removeItemAtURL:_moviewURL error:nil];
    [self initMovieWriter];
    [_videoCamera addTarget:_movieWriter];
}

- (void)initMovieWriter {
    _movieWriter = [[GPUImageMovieWriter alloc]initWithMovieURL:_moviewURL size:CGSizeMake(480.0, 640.0)];
    _movieWriter.encodingLiveVideo = YES;
}

1、摄像头实例取消对GPUImageMovieWriter的绑定,因为重新实例化新的GPUImageMovieWriter以后原来的实例就没用了。
2、删除原来已经写好的影片文件,如果新的实例直接写入已存在的文件会报错AVAssetWriterStatusFailed
3、重新实例化一个GPUImageMovieWriter
4、把新的GPUImageMovieWriter绑定到摄像头实例。

这样以后就可以不同的录制保存了。参考[绍棠] GPUImageMovieWriter 无法2次录像 报错:[AVAssetWriter startWriting] Cannot call method when status is 3

参考资料:落影大佬的GPUImage文集

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