短视频实时滤镜(GPUImageVideoCamera)
demo下载地址:https://github.com/SXDgit/ZB_GPUImageVideoCamera
先看效果图:
直接上代码,后面解释:
- (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
的操作。
GPUImageMovieWriter
是和GPUImageView
处于同一地位的,都是视频输出类,只不过一个是输出到文件,一个输出到屏幕。GPUImageBeautifyFilter
是基于GPUImage的实时美颜滤镜中的美颜滤镜,来自琨君。它是继承于GPUImageFilterGroup
。包括了GPUImageBilateralFilter
、GPUImageCannyEdgeDetectionFilter
、GPUImageCombinationFilter
、GPUImageHSBFilter
。
绘制流程
- 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
显示再屏幕上。
通过源码可以知道GPUImage
是使用AVFoundation
框架来获取视频的。
AVCaptureSession
类从AV输入设备的采集数据到制定的输出。
为了实现实时的图像捕获,要实现AVCaptureSession
类,添加合适的输入(AVCaptureDeviceInput
)和输出(比如AVCaptureMovieFileOutput
)调用startRunning
开始输入到输出的数据流,调用stopRunning
停止数据流。需要注意的是startingRunning
函数会花费一定的时间,所以不能在主线程调用,防止卡顿。
流程解析:
1、找到物理设备摄像头
_inputCamera
、麦克风_microphone
,创建摄像头输入videoInput
和麦克风输入audioInput
。2、设置
videoInput
和audioInput
为_captureSession
的输入,同时设置videoOutput
和audioOutput
为_captureSession
的输出,并且设置videoOutput
和audioOutput
的输出delegate
。3、
_captureSession
调用startRunning
,开始捕获信号。4、音频数据到达,把数据转发给之前设置的
audioEncodingTarget
,并通过调用assetWriterAudioInput
的appendSampleBuffer
方法写入音频数据。5、视频数据到达,视频数据传入响应链,经过处理后通过
assetWriterPixelBufferInput
的appendSampleBuffer
方法写入视频数据。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文集