AVFoundation + OpenGL ES 实现视频滤镜
最近在学习OpenGL,本篇文章是学习OpenGL一段时间后做的练手项目的总结。先来看看最终的效果:
逐渐马赛克.gif
幻影.gif
练手项目就不使用第三方框架了,就使用 AVFoundation 和 OpenGLES 来实现。AVFoudation用来采集摄像头的每帧数据;OpenGLES用于处理特效,并将图像显示到界面上。
(一)AVFoudation 采集视频数据
(1)几个重要的类
AVCaptureSession:整个视频捕捉功能的管理
AVCaptureDevice:捕捉设备,代表摄像头,麦克风等硬件
AVCaptureDeviceInput:AVCaptureDevice不能直接使用,需要包装成 AVCaptureDeviceInput,才能传入AVCaptureSession中
AVCaptureOutput:结果输出类,设置了什么输出,最后就会把捕捉结果以设置的格式输出
a AVCaptureStillImageOutput 输出静态图片
b AVCaputureMovieFileOutput 输出视频
c AVCaputureAudioDataOutput 输出每帧音频数据
d AVCaputureVideoDataOutput 输出每帧视频数据
例如,我只需要用到每帧视频数据,那么设置 AVCaputureVideoDataOutput 就可以了
AVCaptureConnection:代表输入和输出设备之间的连接,设置一些输入或者输出的属性
AVCaptureVideoPreviewLayer:照片/视频捕捉结果的预览图层
(2)初始化和设置 session
基本思路就是创建session,然后将输入设备添加到session中,再设置捕捉之后需要输出的数据格式,然后开启session,就能捕捉到数据了。这里要注意的是改变session的配置时,都需要在改变前后写上 beginConfiguration 和 commitConfiguration 方法。
- (void)setup {
//所有video设备
NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
//前置摄像头
self.frontCamera = [AVCaptureDeviceInput deviceInputWithDevice:videoDevices.lastObject error:nil];
self.backCamera = [AVCaptureDeviceInput deviceInputWithDevice:videoDevices.firstObject error:nil];
//设置当前设备为前置
self.videoInputDevice = self.backCamera;
//视频输出
self.videoDataOutput = [[AVCaptureVideoDataOutput alloc] init];
[self.videoDataOutput setSampleBufferDelegate:self queue:self.captureQueue];
// 丢弃延迟的视频帧
self.videoDataOutput.alwaysDiscardsLateVideoFrames = YES;
// 指定像素的输出格式
self.videoDataOutput.videoSettings = @{
(__bridge NSString *)kCVPixelBufferPixelFormatTypeKey:@(kCVPixelFormatType_420YpCbCr8BiPlanarFullRange)
};
//配置
[self.captureSession beginConfiguration];
if ([self.captureSession canAddInput:self.videoInputDevice]) {
[self.captureSession addInput:self.videoInputDevice];
}
if([self.captureSession canAddOutput:self.videoDataOutput]){
[self.captureSession addOutput:self.videoDataOutput];
}
// 设置分辨率
[self setVideoPreset];
[self.captureSession commitConfiguration];
self.videoConnection = [self.videoDataOutput connectionWithMediaType:AVMediaTypeVideo];
//设置视频输出方向
self.videoConnection.videoOrientation = AVCaptureVideoOrientationPortrait;
// 设置fps
[self updateFps:30];
}
// 设置分辨率
- (void)setVideoPreset{
if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1920x1080]) {
self.captureSession.sessionPreset = AVCaptureSessionPreset1920x1080;
_witdh = 1080; _height = 1920;
}else if ([self.captureSession canSetSessionPreset:AVCaptureSessionPreset1280x720]) {
self.captureSession.sessionPreset = AVCaptureSessionPreset1280x720;
_witdh = 720; _height = 1280;
}else{
self.captureSession.sessionPreset = AVCaptureSessionPreset640x480;
_witdh = 480; _height = 640;
}
}
// 设置fps
-(void)updateFps:(NSInteger) fps{
//获取当前capture设备
NSArray *videoDevices = [AVCaptureDevice devicesWithMediaType:AVMediaTypeVideo];
//遍历所有设备(前后摄像头)
for (AVCaptureDevice *vDevice in videoDevices) {
//获取当前支持的最大fps
float maxRate = [(AVFrameRateRange *)[vDevice.activeFormat.videoSupportedFrameRateRanges objectAtIndex:0] maxFrameRate];
//如果想要设置的fps小于或等于做大fps,就进行修改
if (maxRate >= fps) {
//实际修改fps的代码
if ([vDevice lockForConfiguration:NULL]) {
vDevice.activeVideoMinFrameDuration = CMTimeMake(10, (int)(fps * 10));
vDevice.activeVideoMaxFrameDuration = vDevice.activeVideoMinFrameDuration;
[vDevice unlockForConfiguration];
}
}
}
}
- (AVCaptureSession *)captureSession{
if (!_captureSession) {
_captureSession = [[AVCaptureSession alloc] init];
}
return _captureSession;
}
- (dispatch_queue_t)captureQueue{
if (!_captureQueue) {
_captureQueue = dispatch_queue_create("TMCapture Queue", NULL);
}
return _captureQueue;
}
输出的视频帧以代理方式回调:
-(void)captureOutput:(AVCaptureOutput *)captureOutput didOutputSampleBuffer:(CMSampleBufferRef)sampleBuffer fromConnection:(AVCaptureConnection *)connection {
[self.delegate captureSampleBuffer:sampleBuffer];
}
(二) OpenGLES 处理特效
这一部分内容较多,需要有OpenGL的基础知识,分为如下几个步骤:
a 创建 frameBuffer 和 renderBuffer
b 创建纹理缓冲区,从视频帧数据获取纹理
c 编译链接自定义shader
d 将 attributes,uniforms,texture 传入 shader
e 绘制与显示
首先自定义一个类继承于 CAEAGLLayer(这个类是苹果提供的专门用于显示OpenGL图像数据的layer),提供一个方法接收外部的视频帧数据:
typedef NS_ENUM(NSInteger, LZProgramType) {
LZProgramTypeVertigo, // 幻影
LZProgramTypeRag, // 局部模糊
LZProgramTypeShake, // 抖动
LZProgramTypeMosaic // 马赛克
};
@interface LZDisplayLayer : CAEAGLLayer
// 使用哪一种特效
@property(nonatomic, assign) LZProgramType useProgram;
- (instancetype)initWithFrame:(CGRect)frame;
- (void)displayWithPixelBuffer:(CVPixelBufferRef)pixelBuffer;
@end
(1)创建 frameBuffer 和 renderBuffer
我们最终绘制完成的每帧数据将保存在这两个Buffer中,renderBuffer会与CAEAGLLayer绑定。
- (void)createBuffers
{
// 创建帧缓存区
glGenFramebuffers(1, &_frameBufferHandle);
glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferHandle);
// 创建color缓存区
glGenRenderbuffers(1, &_colorBufferHandle);
glBindRenderbuffer(GL_RENDERBUFFER, _colorBufferHandle);
// 绑定渲染缓存区
[_context renderbufferStorage:GL_RENDERBUFFER fromDrawable:self];
// 得到渲染缓存区的尺寸
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_WIDTH, &_backingWidth);
glGetRenderbufferParameteriv(GL_RENDERBUFFER, GL_RENDERBUFFER_HEIGHT, &_backingHeight);
// 绑定renderBuffer到FrameBuffer
glFramebufferRenderbuffer(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_RENDERBUFFER, _colorBufferHandle);
}
(2)创建纹理缓冲区,从视频帧数据获取纹理
纹理在OpenGL中就代表图像的原始数据(位图),由于视频帧数据是YUV420格式的数据(AVCaptureSession 采集时设置的kCVPixelFormatType_420YpCbCr8BiPlanarFullRange),会有两个平面(Y平面和UV平面),所以对应的纹理也需要创建两个。后面在shader的编写中,会把YUV数据转化为RGB数据。
由于是视频数据渲染比较频繁,所以使用纹理缓冲区,其工作原理就是创建一块专门用于存放纹理的缓冲区,每次创建新的纹理都使用缓冲区的内存,这样不用重新创建,在需要频繁创建纹理时可以提高效率。
创建纹理缓冲区:
/*
CVOpenGLESTextureCacheCreate
功能: 创建 CVOpenGLESTextureCacheRef 创建新的纹理缓存
参数1: kCFAllocatorDefault默认内存分配器.
参数2: NULL
参数3: EAGLContext 图形上下文
参数4: NULL
参数5: 新创建的纹理缓存
@result kCVReturnSuccess
*/
CVReturn err;
err = CVOpenGLESTextureCacheCreate(kCFAllocatorDefault, NULL, _context, NULL, &_videoTextureCache);
if (err != noErr) {
NSLog(@"Error at CVOpenGLESTextureCacheCreate %d", err);
return;
}
创建纹理:
// 返回像素缓冲区的平面数
size_t planeCount = CVPixelBufferGetPlaneCount(pixelBuffer);
/*
从像素缓存区pixelBuffer创建Y和UV纹理,这些纹理会被绘制在帧缓存区的Y平面上.
*/
// 激活纹理
glActiveTexture(GL_TEXTURE0);
// 创建亮度纹理-Y纹理
/*
CVOpenGLESTextureCacheCreateTextureFromImage
功能:根据CVImageBuffer创建CVOpenGlESTexture 纹理对象
参数1: 内存分配器,kCFAllocatorDefault
参数2: 纹理缓存.纹理缓存将管理纹理的纹理缓存对象
参数3: sourceImage.
参数4: 纹理属性.默认给NULL
参数5: 目标纹理,GL_TEXTURE_2D
参数6: 指定纹理中颜色组件的数量(GL_RGBA, GL_LUMINANCE, GL_RGBA8_OES, GL_RG, and GL_RED (NOTE: 在 GLES3 使用 GL_R8 替代 GL_RED).)
参数7: 帧宽度
参数8: 帧高度
参数9: 格式指定像素数据的格式
参数10: 指定像素数据的数据类型,GL_UNSIGNED_BYTE
参数11: planeIndex
参数12: 纹理输出新创建的纹理对象将放置在此处。
*/
CVReturn err;
err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
_videoTextureCache,
pixelBuffer,
NULL,
GL_TEXTURE_2D,
GL_RED_EXT,
frameWidth,
frameHeight,
GL_RED_EXT,
GL_UNSIGNED_BYTE,
0,
&_lumaTexture);
if (err) {
NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
}
// 配置亮度纹理属性
// 绑定纹理.
glBindTexture(CVOpenGLESTextureGetTarget(_lumaTexture), CVOpenGLESTextureGetName(_lumaTexture));
// 配置纹理放大/缩小过滤方式以及纹理围绕S/T环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
// 如果颜色通道个数>1,则除了Y还有UV-Plane.
if(planeCount == 2) {
// 激活UV-plane纹理
glActiveTexture(GL_TEXTURE1);
// 创建UV-plane纹理
/*
CVOpenGLESTextureCacheCreateTextureFromImage
功能:根据CVImageBuffer创建CVOpenGlESTexture 纹理对象
参数1: 内存分配器,kCFAllocatorDefault
参数2: 纹理缓存.纹理缓存将管理纹理的纹理缓存对象
参数3: sourceImage.
参数4: 纹理属性.默认给NULL
参数5: 目标纹理,GL_TEXTURE_2D
参数6: 指定纹理中颜色组件的数量(GL_RGBA, GL_LUMINANCE, GL_RGBA8_OES, GL_RG, and GL_RED (NOTE: 在 GLES3 使用 GL_R8 替代 GL_RED).)
参数7: 帧宽度
参数8: 帧高度
参数9: 格式指定像素数据的格式
参数10: 指定像素数据的数据类型,GL_UNSIGNED_BYTE
参数11: planeIndex
参数12: 纹理输出新创建的纹理对象将放置在此处。
*/
err = CVOpenGLESTextureCacheCreateTextureFromImage(kCFAllocatorDefault,
_videoTextureCache,
pixelBuffer,
NULL,
GL_TEXTURE_2D,
GL_RG_EXT,
frameWidth / 2,
frameHeight / 2,
GL_RG_EXT,
GL_UNSIGNED_BYTE,
1,
&_chromaTexture);
if (err) {
NSLog(@"Error at CVOpenGLESTextureCacheCreateTextureFromImage %d", err);
}
// 绑定纹理
glBindTexture(CVOpenGLESTextureGetTarget(_chromaTexture), CVOpenGLESTextureGetName(_chromaTexture));
// 配置纹理放大/缩小过滤方式以及纹理围绕S/T环绕方式
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR);
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE);
glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE);
}
(3)编译链接自定义shader
特效的实现需要我们自定义片元着色器,使用OpenGL和苹果封装的着色器无法实现,所以需要自己编译链接编写的shader;分为2步:
a 编译shader
b 将shader和program链接
编译 shader:
- (GLuint)compileShaderWithName:(NSString *)name type:(GLenum)shaderType {
//1.获取shader 路径
NSString *shaderPath = [[NSBundle mainBundle] pathForResource:name ofType:shaderType == GL_VERTEX_SHADER ? @"vsh" : @"fsh"];
NSError *error;
NSString *shaderString = [NSString stringWithContentsOfFile:shaderPath encoding:NSUTF8StringEncoding error:&error];
if (!shaderString) {
NSAssert(NO, @"读取shader失败");
exit(1);
}
//2. 创建shader->根据shaderType
GLuint shader = glCreateShader(shaderType);
//3.获取shader source
const char *shaderStringUTF8 = [shaderString UTF8String];
int shaderStringLength = (int)[shaderString length];
glShaderSource(shader, 1, &shaderStringUTF8, &shaderStringLength);
//4.编译shader
glCompileShader(shader);
//5.查看编译是否成功
GLint compileSuccess;
glGetShaderiv(shader, GL_COMPILE_STATUS, &compileSuccess);
if (compileSuccess == GL_FALSE) {
GLchar messages[256];
glGetShaderInfoLog(shader, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSAssert(NO, @"shader编译失败:%@", messageString);
exit(1);
}
//6.返回shader
return shader;
}
将shader和program链接:
- (GLuint)programWithShaderName:(NSString *)shaderName {
//1. 编译顶点着色器/片元着色器
GLuint vertexShader = [self compileShaderWithName:@"Vertex" type:GL_VERTEX_SHADER];
GLuint fragmentShader = [self compileShaderWithName:shaderName type:GL_FRAGMENT_SHADER];
//2. 将顶点/片元附着到program
GLuint program = glCreateProgram();
glAttachShader(program, vertexShader);
glAttachShader(program, fragmentShader);
//3.linkProgram
glLinkProgram(program);
//4.检查是否link成功
GLint linkSuccess;
glGetProgramiv(program, GL_LINK_STATUS, &linkSuccess);
if (linkSuccess == GL_FALSE) {
GLchar messages[256];
glGetProgramInfoLog(program, sizeof(messages), 0, &messages[0]);
NSString *messageString = [NSString stringWithUTF8String:messages];
NSAssert(NO, @"program链接失败:%@", messageString);
exit(1);
}
//5.返回program
return program;
}
(4)将 attributes,uniforms,texture 传入 shader
attributes:顶点数据和纹理数据,确定图像的位置和尺寸,可传入顶点着色器,再籍由顶点着色器传入片元着色器
uniforms:应用传给shader的常量,可传入顶点着色器和片元着色器
texture:纹理id,代表图像数据,可传入顶点着色器和片元着色器,本项目中顶点着色器不会用到纹理,因此只传入片元着色器
顶点数据和纹理数据的计算:
// 根据视频的方向和纵横比设置四边形顶点
CGRect viewBounds = self.bounds;
CGSize contentSize = CGSizeMake(frameWidth, frameHeight);
/*
AVMakeRectWithAspectRatioInsideRect
功能: 返回一个按比例缩放的CGRect,该CGRect保持由边界CGRect内的CGSize指定的纵横比
参数1:希望保持的宽高比或纵横比
参数2:填充的rect
*/
CGRect vertexSamplingRect = AVMakeRectWithAspectRatioInsideRect(contentSize, viewBounds);
// 计算四边形坐标以将帧绘制到其中
CGSize normalizedSamplingSize = CGSizeMake(0.0, 0.0);
CGSize cropScaleAmount = CGSizeMake(vertexSamplingRect.size.width/viewBounds.size.width,vertexSamplingRect.size.height/viewBounds.size.height);
if (cropScaleAmount.width > cropScaleAmount.height) {
normalizedSamplingSize.width = 1.0;
normalizedSamplingSize.height = cropScaleAmount.height/cropScaleAmount.width;
}
else {
normalizedSamplingSize.width = cropScaleAmount.width/cropScaleAmount.height;
normalizedSamplingSize.height = 1.0;;
}
/*
四顶点数据定义了绘制像素缓冲区的二维平面区域。
使用(-1,-1)和(1,1)分别作为左下角和右上角坐标形成的顶点数据覆盖整个屏幕。
*/
GLfloat quadVertexData [] = {
-1 * normalizedSamplingSize.width, -1 * normalizedSamplingSize.height,
normalizedSamplingSize.width, -1 * normalizedSamplingSize.height,
-1 * normalizedSamplingSize.width, normalizedSamplingSize.height,
normalizedSamplingSize.width, normalizedSamplingSize.height,
};
/*
纹理顶点的设置使我们垂直翻转纹理。这使得我们的左上角原点缓冲区匹配OpenGL的左下角纹理坐标系
*/
CGRect textureSamplingRect = CGRectMake(0, 0, 1, 1);
GLfloat quadTextureData[] = {
CGRectGetMinX(textureSamplingRect), CGRectGetMaxY(textureSamplingRect),
CGRectGetMaxX(textureSamplingRect), CGRectGetMaxY(textureSamplingRect),
CGRectGetMinX(textureSamplingRect), CGRectGetMinY(textureSamplingRect),
CGRectGetMaxX(textureSamplingRect), CGRectGetMinY(textureSamplingRect)
};
将 attributes,uniforms,texture 传入 shader:
// 坐标数据
int position = glGetAttribLocation(self.usingProgram, "position");
glVertexAttribPointer(position, 2, GL_FLOAT, 0, 0, quadVertexData);
glEnableVertexAttribArray(position);
// 更新纹理坐标属性值
int texCoord = glGetAttribLocation(self.usingProgram, "texCoord");
glVertexAttribPointer(texCoord, 2, GL_FLOAT, 0, 0, quadTextureData);
glEnableVertexAttribArray(texCoord);
// 使用shaderProgram
glUseProgram(self.program[self.useProgram]);
self.usingProgram = self.program[0];
// 获取uniform的位置
// Y亮度纹理
uniforms[UNIFORM_Y] = glGetUniformLocation(self.usingProgram, "SamplerY");
// UV色量纹理
uniforms[UNIFORM_UV] = glGetUniformLocation(self.usingProgram, "SamplerUV");
// YUV->RGB
uniforms[UNIFORM_COLOR_CONVERSION_MATRIX] = glGetUniformLocation(self.usingProgram, "colorConversionMatrix");
// 时间差
uniforms[UNIFORM_TIME] = glGetUniformLocation(self.usingProgram, "Time");
glUniform1i(uniforms[UNIFORM_Y], 0);
glUniform1i(uniforms[UNIFORM_UV], 1);
glUniformMatrix3fv(uniforms[UNIFORM_COLOR_CONVERSION_MATRIX], 1, GL_FALSE, _preferredConversion);
//传递Uniform属性到shader
//UNIFORM_COLOR_CONVERSION_MATRIX YUV->RGB颜色矩阵
glUniformMatrix3fv(uniforms[UNIFORM_COLOR_CONVERSION_MATRIX], 1, GL_FALSE, _preferredConversion);
// 传入当前时间与绘制开始时间的时间差
NSTimeInterval time = [[NSDate date] timeIntervalSinceDate:self.startDate];
glUniform1f(uniforms[UNIFORM_TIME], time);
(5)绘制与显示
// 绑定帧缓存区
glBindFramebuffer(GL_FRAMEBUFFER, _frameBufferHandle);
// 设置视口.
glViewport(0, 0, _backingWidth, _backingHeight);
// 绘制图形
glDrawArrays(GL_TRIANGLE_STRIP, 0, 4);
// 绑定渲染缓存区
glBindRenderbuffer(GL_RENDERBUFFER, _colorBufferHandle);
// 显示到屏幕
[_context presentRenderbuffer:GL_RENDERBUFFER];
(三)shader 特效
特效是自定义片元着色器编写的,马赛克特效:
逐渐马赛克.gif
precision mediump float;
varying highp vec2 texCoordVarying;
uniform sampler2D SamplerY;
uniform sampler2D SamplerUV;
uniform mat3 colorConversionMatrix;
uniform float Time;
const vec2 TexSize = vec2(375.0, 667.0);
const vec2 mosaicSize = vec2(20.0, 20.0);
const float PI = 3.1415926;
vec4 getRgba(vec2 texCoordVarying) {
mediump vec3 yuv;
lowp vec3 rgb;
yuv.x = (texture2D(SamplerY, texCoordVarying).r - (16.0/255.0));
yuv.yz = (texture2D(SamplerUV, texCoordVarying).rg - vec2(0.5, 0.5));
rgb = colorConversionMatrix * yuv;
return vec4(rgb, 1);
}
void main () {
float duration = 3.0;
float maxScale = 1.0;
float time = mod(Time, duration);
float progress = sin(time * (PI / duration));
float scale = maxScale * progress;
vec2 finSize = mosaicSize * scale;
vec2 intXY = vec2(texCoordVarying.x*TexSize.x, texCoordVarying.y*TexSize.y);
vec2 XYMosaic = vec2(floor(intXY.x/finSize.x)*finSize.x, floor(intXY.y/finSize.y)*finSize.y);
vec2 UVMosaic = vec2(XYMosaic.x/TexSize.x, XYMosaic.y/TexSize.y);
gl_FragColor = getRgba(UVMosaic);
}
自己搞的,实际应该不会有这种特效吧哈哈哈。原理就是把整个纹理当成是一张375x667的图片,把图片一块块小区域,切分区域的大小随时间变化(正弦函数取上半部分)。根据当前像素点的坐标数据可以确定其在哪一块区域,然后像素点的颜色值就取其所在区域左上角第一个像素点的颜色。
幻影.gif局部模糊.gif
抖动.gif
幻影,局部模糊,抖动特效是参考雷曼同学的文章。
至此,从采集视频到添加滤镜整个过程就完成了。完整项目的github地址:
https://github.com/linzhesheng/AVFoundationAndOpenGLES。