经验总结:使用GPUImage实现视频滤镜
关于GPUImage
这里直接引用官方描述:
The GPUImage framework is a BSD-licensed iOS library that lets you apply GPU-accelerated filters and other effects to images, live camera video, and movies. In comparison to Core Image (part of iOS 5.0), GPUImage allows you to write your own custom filters, supports deployment to iOS 4.0, and has a simpler interface. However, it currently lacks some of the more advanced features of Core Image, such as facial detection.
项目地址:https://github.com/BradLarson/GPUImage
(点击“阅读原文”可进入链接)
滤镜基本原理
滤镜处理的原理就是把静态图片或者视频的每一帧进行图形变换再显示出来。它的本质就是像素点的坐标和颜色变化,这点从GPUImage项目中滤镜的源代码就可以了解到。
例如下面就是一个名为DarkenBlend的滤镜代码:
NSString *const kGPUImageDarkenBlendFragmentShaderString = SHADER_STRING
(
varying highp vec2 textureCoordinate;
varying highp vec2 textureCoordinate2;
uniform sampler2D inputImageTexture;
uniform sampler2D inputImageTexture2;
void main()
{
lowp vec4 base = texture2D(inputImageTexture, textureCoordinate);
lowp vec4 overlayer = texture2D(inputImageTexture2, textureCoordinate2);
gl_FragColor = vec4(min(overlayer.rgb * base.a, base.rgb * overlayer.a) + overlayer.rgb * (1.0 - base.a) + base.rgb * (1.0 - overlayer.a), 1.0);
}
);
GPUImage的最新版本已经内置了125个滤镜。也支持编写自定义滤镜(custom filter)。
图像处理流程
数据源需要实现GPUImageOutput接口,而后续各个环节(包括最后处理输出结果)的对象都要实现GPUImageInput接口。每个处理环节都是从上一个环节获取图像数据,进行处理后再将结果传递给下一个。下游的处理对象称为上一步的target。使用addTarget:方法为处理链路添加每个环节的对象。一个常见的链条如下:
数据源(例如GPUImageVideoCamera或者GPUImageMovie)->各类filter->GPUImageView
如果需要将结果输出到文件,只要把上述流程中最末的GPUImageView改为一个GPUImageMovieWriter即可。所以滤镜的添加和替换,以及输出结果都十分简单方便。
附一张GPUImage的结构图:
接入app
将滤镜接入app非常简单,只要创建一个GPUImageMovie,一个具体的GPUImageFilter和一个用来显示的GPUImageView,然后按处理链条的上下游关系把它们用addTarget串起来。
下面附上我的app里面的调用代码(预览视频):
_movie = [[GPUImageMovie alloc] initWithURL:url];
_movie.shouldRepeat = YES;
_movie.playAtActualSpeed = YES;
_movie.playSound = YES;
_movie.delegate = self;
GPUImageMissEtikateFilter *filter = [[GPUImageMissEtikateFilter alloc] init];//胶片效果
[_movie addTarget:filter];
[filter addTarget:self.gpuImageView];
[_movie startProcessing];
然后就是一些重要的参数设置:
playAtActualSpeed
控制GPUImageView预览视频时的速度是否要保持真实的速度。如果设为NO,则会将视频的所有帧无间隔渲染,导致速度非常快。设为YES,则会根据视频本身时长计算出每帧的时间间隔,然后每渲染一帧,就sleep一个时间间隔,从而达到正常的播放速度。
shouldRepeat
控制视频是否循环播放。
当你不想预览,而是想将处理过的结果输出到文件时,步骤也类似,只是不再需要创建GPUImageView,而是需要一个GPUImageMovieWriter:
_writer = [[GPUImageMovieWriter alloc] initWithMovieURL:filtedMovieURL size:CGSizeMake(480, 640) fileType:(NSString *)kUTTypeMPEG4 outputSettings:outputSettings];
_writer.encodingLiveVideo = NO;
_writer.shouldPassthroughAudio = NO;
_movie.audioEncodingTarget = _writer;
_movie.playAtActualSpeed = NO;
[_movie addTarget:_curFilter];
[_curFilter addTarget:_writer];
[_movie enableSynchronizedEncodingUsingMovieWriter:_writer];
[_writer startRecording];
[_movie startProcessing];
一些局限和踩过的坑
1.预览时不支持播放声音
视频结果输出到GPUImageView预览时不支持播放声音,所以要自行添加声音播放:
自行创建了一个播放器对象
- (void) setupSound
{
if (theAudioPlayer != nil)
{
[theAudioPlayer pause];
[theAudioPlayer seekToTime:kCMTimeZero];
theAudioPlayer = nil;
}
theAudioPlayer = [[AVPlayer alloc] initWithURL:self.url];
}
跟GPUImageView的图像同步播放
if (self.playSound && hasAudioTrack)
{
[theAudioPlayer seekToTime:kCMTimeZero];
[theAudioPlayer play];
}
由于GPUImageMovie里面没有在这几个重要位置提供回调,所以只能在源代码中直接添加,这也导致了对源码的侵入,不利于以后版本升级。
2.GPUImageView预览视频并循环播放时,当播放重新开始时,有大约50%的概率会有红屏闪一下,目前还未定位到原因
3.GPUImageView预览视频时,app切到后台会crash,所以要特别处理下,在willDisappear和handleApplicationWillEnterBackgroundNotify这两个时机都要暂停滤镜处理:
if (_movie) {
[_movie endProcessing];
[_movie removeAllTargets];
_movie = nil;
}
对应的,在willAppear和handleApplicationWillEnterForegroundNotify这两个时机要重启处理:
if (!_movie) {
_movie = [[GPUImageMovie alloc] initWithURL:url];
}
_movie.shouldRepeat = YES;
_movie.playAtActualSpeed = YES;
_movie.playSound = YES;
_movie.delegate = self;
[_movie addTarget:_curFilter];
[_curFilter addTarget:self.gpuImageView];
[_movie startProcessing];
更多深度技术内容,请关注云栖社区微信公众号:yunqiinsight。